12k
All articles

VitestでVueテストにおけるAPIコールのモック化

VitestでVueテストのAPIコールをモック化する手法として、モジュールレベルのモックとMock Service WorkerによるHTTPレイヤー傍受を解説する。

OpenReplay Team
OpenReplay Team
VitestでVueテストにおけるAPIコールのモック化

Vueコンポーネントが単独で動作することは稀です。ほとんどのコンポーネントはデータの取得、フォームの送信、またはエンドポイントのポーリングを行います。ネットワークを制御せずにこれらのコンポーネントをテストすると、テストが遅くなり、不安定になり、外部サービスに依存することになります。VueテストでAPIコールをモック化することで、コンポーネントが受け取るレスポンスを制御できるようになり、これらの問題を解消できます。

この記事では、Vitest VueでAPIをモック化する2つの実践的な戦略を取り上げます。vi.mockを使用してAPIモジュールを直接置き換える方法と、Mock Service Worker (MSW)を使用してHTTPレイヤーでリクエストをインターセプトする方法です。また、両方のアプローチを確実に機能させるために必要なVue固有の非同期処理についても学びます。

重要なポイント

  • テストにおける実際のネットワークコールは、不安定で遅く、非決定的な結果を生み出します — モック化によってレスポンスを完全に制御できます。
  • vi.mockはインポートレベルでAPIモジュールを置き換えるため、高速で分離されたユニットテストに最適です。
  • MSWはネットワークレイヤーでリクエストをインターセプトし、実際のfetchやaxiosロジックを実行させることで、よりリアルな統合テストを実現します。
  • @vue/test-utilsflushPromises()は、コンポーネントがDOMを更新する前に非同期リクエストを待機する場合に一般的に必要です。

VitestでVueコンポーネントテストにおいてAPIコールをモック化する必要がある理由

テストにおける実際のネットワークコールは非決定的な結果を生み出します。外部APIが遅い、または利用できないという理由だけで、同じテストがローカルでは成功し、CIでは失敗することがあります。モック化によってネットワークが返す内容を制御できるため、テストが高速で予測可能な状態を保てます。

戦略1: vi.mockによるAPIモジュールのモック化

最も直接的なアプローチは、コンポーネントがHTTPコールを認識する前に、それを担当するモジュールを置き換えることです。

// quote.service.ts
export async function fetchQuote() {
  const response = await fetch('https://api.example.com/quotes/random')
  return response.json()
}
// QuoteCard.test.ts
import { mount, flushPromises } from '@vue/test-utils'
import { vi, test, expect } from 'vitest'
import { fetchQuote } from './quote.service'
import QuoteCard from './QuoteCard.vue'

// vi.mockは自動的にインポートより上に巻き上げられます
vi.mock('./quote.service')

test('マウント後に引用文を表示する', async () => {
  vi.mocked(fetchQuote).mockResolvedValue({
    id: 1,
    quote: 'テスト用引用文',
    author: 'テスター',
  })

  const wrapper = mount(QuoteCard)

  // すべての保留中のPromiseとDOM更新が完了するまで待機
  await flushPromises()

  expect(wrapper.text()).toContain('テスト用引用文')
})

重要な詳細: vi.mockはVitestによってファイルの先頭に巻き上げられるため、すべてのインポートより前に実行されます。これはVitestモッキングAPIで文書化されている意図的な動作です。つまり、実際の関数を安全にインポートしても、テスト内ではモック化されたバージョンを受け取ることができます。

このパターンではflushPromises()の呼び出しが必要になることがよくあります。 マウント時にデータを取得するVueコンポーネントは、DOMを非同期的に更新します。flushPromisesのようなユーティリティは、アサーションが実行される前に保留中のPromiseが解決されることを保証し、ローディング状態に対してアサートすることを防ぎます。

このアプローチを使用するタイミング: 特定のコンポーネントやサービスを分離してユニットテストする場合で、戻り値や呼び出し回数を厳密に制御したい場合。

戦略2: HTTPレイヤーモッキングのためのMSWとVitest Vueテスト

Mock Service Workerは、JavaScriptモジュールを置き換えるのではなく、ネットワークレベルでリクエストをインターセプトします。これにより、実際のfetchやaxiosコールが実行されるため、テストがよりリアルになります — MSWはそれがプロセスを離れる前にインターセプトするだけです。

MSWをインストールし、Node環境(Vitestのデフォルト)用にセットアップします:

npm install -D msw
// test/server.ts
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'

export const server = setupServer(
  http.get('https://api.example.com/quotes/random', () => {
    return HttpResponse.json({ id: 1, quote: 'MSW引用文', author: 'MSW' })
  })
)
// QuoteCard.test.ts
import { mount, flushPromises } from '@vue/test-utils'
import { test, expect, beforeAll, afterEach, afterAll } from 'vitest'
import { server } from './test/server'
import QuoteCard from './QuoteCard.vue'

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

test('MSW経由で取得した引用文を表示する', async () => {
  const wrapper = mount(QuoteCard)
  await flushPromises()
  expect(wrapper.text()).toContain('MSW引用文')
})

ここでも同じ理由でflushPromises()が必要になる場合があります — Vueのリアクティビティシステムは、Promiseが解決された後にDOM更新を適用するためにティックが必要です。

このアプローチを使用するタイミング: コンポーネントがリアルなHTTP動作で機能することを検証したい統合スタイルのテスト、または複数のコンポーネントが同じエンドポイントを共有する場合。Vitest自身のドキュメントでは、Nodeベースのテスト環境でのリクエストレベルのモッキングにMSWを推奨しています。

vi.mockとMSWの選択

vi.mockMSW
インターセプト層モジュールレベルネットワークレベル
実際のfetchロジックをテスト
テストごとの制御✅ 簡単server.use()経由
最適な用途ユニットテスト統合テスト

結論

どちらの戦略もVueテストでAPIコールをモック化するための有効なツールです。高速で分離されたユニットテストで戻り値を正確に制御したい場合はvi.mockを使用してください。完全なリクエストパスを実行するテストが必要な場合はMSWを使用してください。いずれの場合も、flushPromises()のようなユーティリティは、アサーションが実行される前にVueが非同期更新のレンダリングを完了することを保証するのに役立ちます。

よくある質問

同じプロジェクトでvi.mockとMSWを一緒に使用できますか?

はい。多くのチームは、個々のコンポーネントの集中的なユニットテストにvi.mockを使用し、複数のコンポーネントやサービスにまたがる広範な統合テストにMSWを使用しています。どのレイヤーがモックを処理しているかについての混乱を避けるため、別々のテストファイルに保管してください。

flushPromisesなしでテストが成功するのに、コンソールに警告が表示されるのはなぜですか?

アサーションが実行されるときに、Vueがまだローディング状態をレンダリングしている可能性があります。アサーションがたまたまローディングテキストと一致する場合、テストは偶然成功します。コンソール警告は通常、未処理のPromiseリジェクションや不足しているawaitを示します。非同期リクエストをトリガーするコンポーネントをマウントした後にflushPromisesを呼び出すことで、これらのタイミング問題を回避できます。

MSWはaxiosでも動作しますか、それともネイティブfetch APIのみですか?

MSWはネットワークレベルでインターセプトするため、axios、fetch、およびkyやgotのようにそれらの上に構築されたライブラリを含む、あらゆるHTTPクライアントで動作します。HTTPクライアントの選択は、MSWがインターセプションを処理する方法に影響しません。

VueテストでAPIエラーレスポンスをシミュレートするにはどうすればよいですか?

vi.mockの場合は、mockRejectedValueを呼び出して非同期エラーのスローをシミュレートします。MSWの場合は、特定のテスト内でserver.useを使用して、4xxまたは5xxステータスコードを持つHttpResponse.jsonを返すハンドラーを登録します。どちらのアプローチでも、コンポーネントのエラー処理を検証できます。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.