2026年のVue 3テスト入門:Vitest、Vue Test Utils、そして面接質問

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

2026年のVitestとVue Test Utilsを用いたVueテストのワークフロー

Vueのテストは、自信を持ってリファクタリングできる状態と、壊れやすい当て推量に頼る状態とを分ける要素です。そして2026年には、ツールチェーンが2つのライブラリに集約されました。ランナーとしてのVitestと、コンポーネントをマウントするためのVue Test Utilsです。本ガイドでは、このスタックの設定方法、コンポーネントとコンポーザブルのテスト、依存関係のモック、そして採用チームが実際に尋ねるVueテストの面接質問への答え方までを順を追って解説します。

2026年のモダンなVueテストスタック

Vue公式ドキュメントが推奨する標準構成は、ユニットテストとコンポーネントテストにVitest、低レベルのマウントAPIとしてVue Test Utils、そしてエンドツーエンドのカバレッジにPlaywrightまたはCypressを用いるものです。Vitestはアプリと同じVite設定を再利用するため、テストはまったく同じ変換パイプラインを通り、ほぼ瞬時のホットリロードで実行されます。

Vue 3プロジェクト向けにVitestを設定する

VitestはViteのパイプラインを共有します。つまり、1つの設定ファイルが開発サーバーとテストランナーの両方を駆動します。コンポーネントテストがレンダリング先のDOMを持てるように、environmentオプションはjsdomまたはhappy-domに設定する必要があります。globals: trueフラグを立てると、describeitexpectをファイルごとにインポートせずに利用できます。

vitest.config.tstypescript
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-vuejsdomをインストールした後、上記の設定がそのまま動作します。エイリアスがアプリケーション側の設定を反映しているため、@/composables/useCartのようなインポートはテストでも本番でもまったく同じように解決されます。

Vue Test Utilsでコンポーネントをマウントする

Vue Test Utilsは2つのエントリーポイントを提供します。コンポーネントツリー全体をレンダリングするmountと、子コンポーネントをスタブ化するshallowMountです。ほとんどのユニットテストでは、実際のレンダリング挙動を検証できるためmountが推奨されます。返されるwrapperには、findgettextといったクエリヘルパーが備わっており、レンダリング結果に対してアサーションを行えます。

PriceTag.spec.tstypescript
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ヘルパーはコンポーネントが発行したすべてのカスタムイベントを記録しており、これによって親子間の契約を検証できます。

SearchBar.spec.tstypescript
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はリアクティブな更新を非同期にまとめて処理します。triggersetValueの呼び出しにawaitを付け忘れることは、Vueテストが不安定になる最も一般的な原因です。DOMが再レンダリングされる前にアサーションが実行され、古い出力を読み取ってしまうためです。イベントヘルパー以外の場所で状態が変化する場合は、vueからnextTickをインポートし、アサーションの前にawait nextTick()を実行してください。

Vueコンポーザブルを単独でテストする

リアクティビティAPI(refcomputedwatch)のみを使うコンポーザブルは、コンポーネントをまったく必要としません。これらはリアクティブな状態を返す単なる関数なので、直接呼び出して.valueに対してアサーションするだけで十分です。これはビジネスロジックをカバーする最も高速かつ焦点の定まった方法であり、Vue 3の高度なコンポーザブルで紹介されているパターンとも相性がよいものです。

useCart.spec.tstypescript
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をラップするモジュールをモックしているため、テスト対象のコンポーザブルはサーバーに一切アクセスすることなく、制御されたデータを受け取ります。

useProducts.spec.tstypescript
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のテストガイドは、コンポーネントレベルのテストにこのアプローチを推奨しています。

CheckoutButton.spec.tstypescript
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プロバイダーによって組み込みで提供され、継続的インテグレーションのパイプラインにきれいに統合できます。そこでは、テストの失敗やカバレッジの低下によってマージをブロックできます。

vitest.config.ts (coverage section)typescript
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はリアクティブな変更を次のティックで適用します。triggersetValueのようなイベントヘルパーは、DOMの更新後に解決するPromiseを返すため、それらをawaitすれば十分です。これらのヘルパー以外で変更された状態については、await nextTick()によってアサーション実行前にキューを強制的にフラッシュします。

findgetの違いは何ですか。 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を優先します。
  • 不安定でタイミングに依存するテストをなくすため、アサーションの前に必ずtriggersetValuenextTickをawaitします。
  • リアクティビティAPIのみを使うコンポーザブルは、直接呼び出して.valueに対してアサーションすることでテストします。
  • ネットワークの挙動を決定的にし、エラー経路をテスト可能にするため、vi.mockによってモジュールの境界でモックします。
  • コンポーネントテストにはcreateTestingPinia({ createSpy: vi.fn })を、ストアのロジックを検証する際にはstubActions: falseを使います。
  • data-test属性を通じて要素をクエリし、テストがスタイリングではなく実際の挙動変更でのみ失敗するようにします。

これらのパターンを習得すれば、テストは面倒な作業ではなく設計のツールとなり、リグレッションが本番に到達する前に捕捉できるようになります。面接に備えたスキルを積み上げ続けるには、VueとNuxtの学習パスの全体をぜひご覧ください。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

タグ

#vue
#testing
#vitest
#vue-test-utils
#interview

共有

関連記事