2026年のVue 3テスト入門:Vitest、Vue Test Utils、そして面接質問
2026年のVueテストを実践的に解説します。Vitestの設定、Vue Test Utilsによるコンポーネントのマウント、コンポーザブルやPiniaストアのテスト、APIのモック、カバレッジ計測、そして採用チームが実際に尋ねる面接質問までを網羅します。

Vueのテストは、自信を持ってリファクタリングできる状態と、壊れやすい当て推量に頼る状態とを分ける要素です。そして2026年には、ツールチェーンが2つのライブラリに集約されました。ランナーとしてのVitestと、コンポーネントをマウントするためのVue Test Utilsです。本ガイドでは、このスタックの設定方法、コンポーネントとコンポーザブルのテスト、依存関係のモック、そして採用チームが実際に尋ねるVueテストの面接質問への答え方までを順を追って解説します。
Vue公式ドキュメントが推奨する標準構成は、ユニットテストとコンポーネントテストにVitest、低レベルのマウントAPIとしてVue Test Utils、そしてエンドツーエンドのカバレッジにPlaywrightまたはCypressを用いるものです。Vitestはアプリと同じVite設定を再利用するため、テストはまったく同じ変換パイプラインを通り、ほぼ瞬時のホットリロードで実行されます。
Vue 3プロジェクト向けにVitestを設定する
VitestはViteのパイプラインを共有します。つまり、1つの設定ファイルが開発サーバーとテストランナーの両方を駆動します。コンポーネントテストがレンダリング先のDOMを持てるように、environmentオプションはjsdomまたはhappy-domに設定する必要があります。globals: trueフラグを立てると、describe、it、expectをファイルごとにインポートせずに利用できます。
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath } from 'node:url'
export default defineConfig({
plugins: [vue()],
test: {
// jsdom provides document/window so components can mount
environment: 'jsdom',
// expose describe/it/expect globally
globals: true,
// run this before each test file (matchers, cleanup)
setupFiles: ['./tests/setup.ts'],
// only pick up *.spec.ts and *.test.ts files
include: ['**/*.{test,spec}.ts'],
},
resolve: {
alias: {
// mirror the @ alias used across the app
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})Vitest 3であれば、vitest、@vue/test-utils、@vitejs/plugin-vue、jsdomをインストールした後、上記の設定がそのまま動作します。エイリアスがアプリケーション側の設定を反映しているため、@/composables/useCartのようなインポートはテストでも本番でもまったく同じように解決されます。
Vue Test Utilsでコンポーネントをマウントする
Vue Test Utilsは2つのエントリーポイントを提供します。コンポーネントツリー全体をレンダリングするmountと、子コンポーネントをスタブ化するshallowMountです。ほとんどのユニットテストでは、実際のレンダリング挙動を検証できるためmountが推奨されます。返されるwrapperには、find、get、textといったクエリヘルパーが備わっており、レンダリング結果に対してアサーションを行えます。
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import PriceTag from '@/components/PriceTag.vue'
describe('PriceTag', () => {
it('formats the amount as currency', () => {
const wrapper = mount(PriceTag, {
// props are passed through the mounting options
props: { amount: 4200, currency: 'EUR' },
})
// get() throws if the selector is missing, unlike find()
expect(wrapper.get('[data-test="price"]').text()).toBe('€42.00')
})
it('applies a discount class when on sale', () => {
const wrapper = mount(PriceTag, {
props: { amount: 4200, currency: 'EUR', onSale: true },
})
// classes() returns the array of applied CSS classes
expect(wrapper.get('[data-test="price"]').classes()).toContain('price--sale')
})
})CSSクラスではなくdata-test属性を通じて要素を対象にすることで、テストの堅牢性が保たれます。.price--saleへのスタイリング変更ではセレクタが壊れず、本当の挙動変更があった場合にのみテストが失敗します。この分離は、保守しやすいVueテストで繰り返し登場するテーマです。
ユーザーイベントと発行イベントをテストする
インタラクティブなコンポーネントでは、クリックや入力の後に何が起こるかをアサーションする必要があります。Vue Test UtilsのtriggerはPromiseを返し、それをawaitするとVueのリアクティビティキューがフラッシュされ、DOMが更新を反映します。emittedヘルパーはコンポーネントが発行したすべてのカスタムイベントを記録しており、これによって親子間の契約を検証できます。
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import SearchBar from '@/components/SearchBar.vue'
describe('SearchBar', () => {
it('emits search with the typed query', async () => {
const wrapper = mount(SearchBar)
// setValue writes to the input and fires an input event
await wrapper.get('input').setValue('vitest')
// awaiting trigger flushes the reactivity queue before assertions
await wrapper.get('[data-test="submit"]').trigger('click')
// emitted() returns a record of every event and its payloads
const events = wrapper.emitted('search')
expect(events).toHaveLength(1)
// events[0] is the first emission, [0] its first argument
expect(events![0][0]).toBe('vitest')
})
})Vueはリアクティブな更新を非同期にまとめて処理します。triggerやsetValueの呼び出しにawaitを付け忘れることは、Vueテストが不安定になる最も一般的な原因です。DOMが再レンダリングされる前にアサーションが実行され、古い出力を読み取ってしまうためです。イベントヘルパー以外の場所で状態が変化する場合は、vueからnextTickをインポートし、アサーションの前にawait nextTick()を実行してください。
Vueコンポーザブルを単独でテストする
リアクティビティAPI(ref、computed、watch)のみを使うコンポーザブルは、コンポーネントをまったく必要としません。これらはリアクティブな状態を返す単なる関数なので、直接呼び出して.valueに対してアサーションするだけで十分です。これはビジネスロジックをカバーする最も高速かつ焦点の定まった方法であり、Vue 3の高度なコンポーザブルで紹介されているパターンとも相性がよいものです。
import { describe, it, expect } from 'vitest'
import { useCart } from '@/composables/useCart'
describe('useCart', () => {
it('tracks total price as items change', () => {
// composables using only ref/computed run without mounting
const { items, total, addItem } = useCart()
expect(total.value).toBe(0)
addItem({ id: 1, price: 1500, qty: 2 })
// computed total recalculates synchronously on state change
expect(total.value).toBe(3000)
addItem({ id: 2, price: 500, qty: 1 })
expect(items.value).toHaveLength(2)
expect(total.value).toBe(3500)
})
})例外は、onMountedのようなライフサイクルフックに依存するコンポーザブルです。これらはコンポーネントインスタンスの内部で実行される必要があります。一般的な回避策は、setup内でコンポーザブルを呼び出して結果を公開する小さなヘルパーコンポーネントを用意し、それをVue Test Utilsでマウントする方法です。
Vue.js / Nuxt.jsの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
vi.mockでAPI呼び出しをモックする
実際のネットワークリクエストは、テストを遅く、かつ非決定的にします。Vitestはvi.mockでモジュールを置き換え、vi.fnでスパイ関数を生成します。以下のパターンでは、fetchをラップするモジュールをモックしているため、テスト対象のコンポーザブルはサーバーに一切アクセスすることなく、制御されたデータを受け取ります。
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useProducts } from '@/composables/useProducts'
import { getProducts } from '@/api/products'
// replace the whole module with mocked exports
vi.mock('@/api/products', () => ({
getProducts: vi.fn(),
}))
describe('useProducts', () => {
beforeEach(() => {
// reset call history between tests to avoid leakage
vi.mocked(getProducts).mockReset()
})
it('exposes fetched products and clears loading', async () => {
// define what the mocked API returns for this test
vi.mocked(getProducts).mockResolvedValue([
{ id: 1, name: 'Keyboard' },
])
const { products, loading, load } = useProducts()
await load()
expect(getProducts).toHaveBeenCalledOnce()
expect(loading.value).toBe(false)
expect(products.value[0].name).toBe('Keyboard')
})
})モジュールの境界でモックすることで、コンポーザブルの内部ロジックには手を触れず、各テストがエラー経路を含めてAPIレスポンスを完全に制御できます。代わりにmockRejectedValueを呼び出せば、失敗時にエラー状態が設定されスピナーが停止することを、1つのテストで検証できます。
2026年におけるPiniaストアのテスト
ストアはコンポーネントをまたいだ状態を保持します。公式の@pinia/testingパッケージは、そのテストを容易にします。createTestingPiniaはデフォルトですべてのアクションをスタブ化するため、コンポーネントテストは副作用を実行することなく、アクションがディスパッチされたことをアサーションできます。Piniaのテストガイドは、コンポーネントレベルのテストにこのアプローチを推奨しています。
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import CheckoutButton from '@/components/CheckoutButton.vue'
import { useCartStore } from '@/stores/cart'
describe('CheckoutButton', () => {
it('dispatches checkout on click', async () => {
const wrapper = mount(CheckoutButton, {
global: {
plugins: [
// vi.fn spies on actions instead of executing them
createTestingPinia({ createSpy: vi.fn }),
],
},
})
const store = useCartStore()
await wrapper.get('[data-test="checkout"]').trigger('click')
// the action is stubbed, so only the dispatch is asserted
expect(store.checkout).toHaveBeenCalledOnce()
})
})コンポーネント統合ではなくストアのロジックそのものをテストしたい場合は、stubActions: falseを渡してアクションを通常どおり実行させ、その結果として得られる状態に対してアサーションします。このように、アクションをスタブ化したコンポーネントテストと、アクションを実行するストアテストを分けることで、それぞれのスイートが単一の責務に集中できます。
ウォッチモードでのテスト実行とカバレッジ計測
Vitestには、保存されたファイルの影響を受けるテストだけを再実行するウォッチモードが備わっており、開発中のフィードバックループを数分の1秒にまで短縮します。カバレッジレポートは@vitest/coverage-v8プロバイダーによって組み込みで提供され、継続的インテグレーションのパイプラインにきれいに統合できます。そこでは、テストの失敗やカバレッジの低下によってマージをブロックできます。
export default defineConfig({
test: {
coverage: {
// v8 is the fastest provider and needs no instrumentation step
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
// fail CI when a threshold drops below the target
thresholds: {
statements: 80,
branches: 75,
functions: 80,
},
// exclude generated and config files from the report
exclude: ['**/*.config.ts', '**/types/**'],
},
},
})vitest単体で実行するとローカルではウォッチモードが起動し、一方vitest run --coverageはCIに適した単一パスの実行を行います。明示的なしきい値を設定することで、カバレッジは見せかけの指標から、強制力のある品質ゲートへと変わります。ブランチカバレッジを75パーセント未満に下げるプルリクエストは自動的に失敗するため、貢献者はハッピーパスだけでなく、自分が追加した経路もテストするよう促されます。
よくあるVueテストの面接質問と回答
テストに関する質問は、候補者が保守性についてどう考えているかを明らかにするため、ほとんどのシニアVue面接で登場します。以下の回答は、最も頻出する概念を扱っています。全問はVueテストの面接モジュールで徹底的に演習できます。
mountではなくshallowMountをいつ使うべきですか。 コンポーネントが重い、あるいはすでにテスト済みの子をレンダリングしており、テストが親自身のロジックだけを対象とする場合にshallowMountが有効です。子をスタブ化することでユニットを分離し、レンダリングを高速化できますが、その代償としてコンポーネント間の実際の統合は検証されません。
Vue Test Utilsでは非同期更新をどう扱いますか。 Vueはリアクティブな変更を次のティックで適用します。triggerやsetValueのようなイベントヘルパーは、DOMの更新後に解決するPromiseを返すため、それらをawaitすれば十分です。これらのヘルパー以外で変更された状態については、await nextTick()によってアサーション実行前にキューを強制的にフラッシュします。
findとgetの違いは何ですか。 findは一致する要素がない場合に空のラッパーを返し、決して例外を投げません。これはexpect(wrapper.find('.error').exists()).toBe(false)のようなアサーションに適しています。getは要素が見つからないとただちに例外を投げるため、要素の存在が前提であり、その欠如が本当の失敗となる場合に適した選択肢です。
なぜクラスセレクタよりdata-test属性を優先するのですか。 クラス名はスタイリングや構造の変更に伴って変わるため、それらをクエリするテストは見た目の修正で壊れてしまいます。専用のdata-testフックはテストの意図を明示的に表現し、リファクタリングに耐えるため、誤検知を減らします。
onMountedに依存するコンポーザブルはどうテストしますか。 ライフサイクルフックはコンポーネントインスタンスの内部でのみ発火するため、コンポーザブルを直接呼び出すことはできません。標準的な手法は、setup内でコンポーザブルを呼び出して戻り値を公開する使い捨てのハーネスコンポーネントを用意し、それをVue Test Utilsでマウントしてラッパー経由でアサーションする方法です。これにより、テスト対象のロジックを分離しつつ、リアクティビティとライフサイクルの配線をそのまま保てます。より概念的な質問やコーディングの質問は、Vue.jsの必須面接質問ガイドにまとめられています。
まとめ
2026年における信頼できるVueテスト環境は、いくつかの意図的な選択に集約されます。
environment: 'jsdom'とVueプラグインを設定してVitestを実行し、テストがアプリとまったく同じViteの変換パイプラインを共有するようにします。- 子コンポーネントが重い、あるいは別の場所ですでにカバーされている場合を除き、
shallowMountよりmountを優先します。 - 不安定でタイミングに依存するテストをなくすため、アサーションの前に必ず
trigger、setValue、nextTickをawaitします。 - リアクティビティAPIのみを使うコンポーザブルは、直接呼び出して
.valueに対してアサーションすることでテストします。 - ネットワークの挙動を決定的にし、エラー経路をテスト可能にするため、
vi.mockによってモジュールの境界でモックします。 - コンポーネントテストには
createTestingPinia({ createSpy: vi.fn })を、ストアのロジックを検証する際にはstubActions: falseを使います。 data-test属性を通じて要素をクエリし、テストがスタイリングではなく実際の挙動変更でのみ失敗するようにします。
これらのパターンを習得すれば、テストは面倒な作業ではなく設計のツールとなり、リグレッションが本番に到達する前に捕捉できるようになります。面接に備えたスキルを積み上げ続けるには、VueとNuxtの学習パスの全体をぜひご覧ください。
今すぐ練習を始めましょう!
面接シミュレーターと技術テストで知識をテストしましょう。
タグ
共有
関連記事

Vue 3 コンポーザブル上級ガイド:再利用可能なパターンと技術面接の質問 2026
Vue 3の高度なコンポーザブルパターンを網羅的に解説。非同期処理、依存性注入、フォームバリデーション、テスト手法、2026年の技術面接で頻出する質問と回答を紹介します。

Vue 3 Pinia vs Vuex徹底比較:2026年の状態管理と面接対策ガイド
Vue 3におけるPiniaとVuexの違いを徹底的に比較します。Options StoreとSetup Store、TypeScript統合、ストア間通信、Vuexからの移行手順、SSR対応まで、2026年の面接で問われる状態管理の知識を実践的なコード例とともに解説します。

Nuxt 4の新機能を徹底解説:ディレクトリ構造の刷新とNuxt 3からの移行ガイド(2026年版)
Nuxt 4で導入されたapp/ディレクトリ構造、シングルトンデータフェッチ層、shallow reactivity、TypeScript分割コンテキストについて、コード例を交えて詳しく解説します。