# 2026年のVue 3テスト入門:Vitest、Vue Test Utils、そして面接質問 > 2026年のVueテストを実践的に解説します。Vitestの設定、Vue Test Utilsによるコンポーネントのマウント、コンポーザブルやPiniaストアのテスト、APIのモック、カバレッジ計測、そして採用チームが実際に尋ねる面接質問までを網羅します。 - Published: 2026-06-21 - Updated: 2026-07-06 - Author: SharpSkill - Tags: vue, testing, vitest, vue-test-utils, interview - Reading time: 10 min --- Vueのテストは、自信を持ってリファクタリングできる状態と、壊れやすい当て推量に頼る状態とを分ける要素です。そして2026年には、ツールチェーンが2つのライブラリに集約されました。ランナーとしての[Vitest](https://vitest.dev/guide/)と、コンポーネントをマウントするための[Vue Test Utils](https://test-utils.vuejs.org/)です。本ガイドでは、このスタックの設定方法、コンポーネントとコンポーザブルのテスト、依存関係のモック、そして採用チームが実際に尋ねるVueテストの面接質問への答え方までを順を追って解説します。 > **2026年のモダンなVueテストスタック** > > [Vue公式ドキュメント](https://vuejs.org/guide/scaling-up/testing.html)が推奨する標準構成は、ユニットテストとコンポーネントテストに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`をファイルごとにインポートせずに利用できます。 ```typescript // vitest.config.ts 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`といったクエリヘルパーが備わっており、レンダリング結果に対してアサーションを行えます。 ```typescript // PriceTag.spec.ts 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`ヘルパーはコンポーネントが発行したすべてのカスタムイベントを記録しており、これによって親子間の契約を検証できます。 ```typescript // SearchBar.spec.ts 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') }) }) ``` > **DOMの更新は必ずawaitする** > > Vueはリアクティブな更新を非同期にまとめて処理します。`trigger`や`setValue`の呼び出しに`await`を付け忘れることは、Vueテストが不安定になる最も一般的な原因です。DOMが再レンダリングされる前にアサーションが実行され、古い出力を読み取ってしまうためです。イベントヘルパー以外の場所で状態が変化する場合は、`vue`から`nextTick`をインポートし、アサーションの前に`await nextTick()`を実行してください。 ## Vueコンポーザブルを単独でテストする リアクティビティAPI(`ref`、`computed`、`watch`)のみを使うコンポーザブルは、コンポーネントをまったく必要としません。これらはリアクティブな状態を返す単なる関数なので、直接呼び出して`.value`に対してアサーションするだけで十分です。これはビジネスロジックをカバーする最も高速かつ焦点の定まった方法であり、[Vue 3の高度なコンポーザブル](/blog/vue-nuxt/advanced-vue-3-composables-reusable-patterns)で紹介されているパターンとも相性がよいものです。 ```typescript // useCart.spec.ts 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でマウントする方法です。 ## vi.mockでAPI呼び出しをモックする 実際のネットワークリクエストは、テストを遅く、かつ非決定的にします。Vitestは`vi.mock`でモジュールを置き換え、`vi.fn`でスパイ関数を生成します。以下のパターンでは、`fetch`をラップするモジュールをモックしているため、テスト対象のコンポーザブルはサーバーに一切アクセスすることなく、制御されたデータを受け取ります。 ```typescript // useProducts.spec.ts 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のテストガイド](https://pinia.vuejs.org/cookbook/testing.html)は、コンポーネントレベルのテストにこのアプローチを推奨しています。 ```typescript // CheckoutButton.spec.ts 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`プロバイダーによって組み込みで提供され、継続的インテグレーションのパイプラインにきれいに統合できます。そこでは、テストの失敗やカバレッジの低下によってマージをブロックできます。 ```typescript // vitest.config.ts (coverage section) 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テストの面接モジュール](/technologies/vue-nuxt/interview-questions/vue-testing)で徹底的に演習できます。 **`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の必須面接質問](/blog/vue-nuxt/essential-vuejs-interview-questions)ガイドにまとめられています。 ## まとめ 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の学習パス](/technologies/vue-nuxt)の全体をぜひご覧ください。 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ja/blog/vue-nuxt/vue-3-testing-vitest-vue-test-utils