目次
背景
自動テストは、ソフトウェア開発において品質を保証するための重要な手段です。
自動テストには、主に ユニットテスト / インテグレーションテスト / E2E (End to End) テスト の3種類があり、それぞれ異なるレベルでアプリケーションの動作を検証します。
- ユニットテスト:個々の関数やコンポーネントが期待通りに動作するかを確認
- インテグレーションテスト:複数のコンポーネントが連携して正しく動作するかを検証
- E2Eテスト:ユーザーの視点からアプリケーション全体の動作を確認
Web アプリケーションの場合、一つの画面や機能を一つの関数やコンポーネントで完結させることはほとんどなく、複数の関数やコンポーネントが連携して動作することにより、初めて一つの画面や機能が実現されます。
そのため、Web アプリケーションのテストにおいては、ユーザーに近い視点でアプリケーションの動作を検証できるテストが重要となり、インテグレーションテストや E2E テストの役割が大きくなります。
しかし、E2E テストは実際のバックエンド API や DB などの外部要素に依存することから、以下のような課題があります。
- テストの実行速度が遅い
- テストの安定性が低い (外部要素の状態に依存し、結果が変わりやすい)
- テスト環境の構築や管理が複雑になる
つまり、Web アプリケーションのテストでは
- ユーザーに近い視点で動作を検証できる
- E2E の課題 (速度・安定性・環境) を避けられる
という点から、インテグレーションテストを充実させることが最も重要になります。
Testing Library の開発者である Kent C. Dodds 氏も Testing JavaScript の中で、Web アプリケーションにおけるインテグレーションテストの重要性を強調しています。
今回紹介する構成
今回紹介するのは、Playwright + MSW (Mock Service Worker) を組み合わせることで、
- ユーザーに近い視点でアプリケーションの動作を検証しつつ
- 外部要素に依存しない安定したインテグレーションテストを実現する方法
です。
Playwright とは
Playwright は、Microsoft が開発したオープンソースの E2E テストフレームワークです。
Playwright は、以下のような特徴を持っています。
- クロスブラウザ対応: Chromium、Firefox、WebKit など複数のブラウザでテストを実行可能
- ヘッドレスモード: GUI を表示せずに高速にテストを実行可能
- 強力なセレクタ: CSS セレクタやテキストセレクタなど、柔軟な要素選択が可能
- 自動待機: 要素の表示や状態変化を自動的に待機する機能
Playwright の他にも、Cypress や Puppeteer などの E2E テストフレームワークがありますが、Playwright はクロスブラウザ対応や自動待機機能が優れており、近年人気が高まっています。
MSW (Mock Service Worker) とは
MSW (Mock Service Worker) は、ブラウザや Node.js 環境で動作する API モックライブラリです。
MSW は、以下のような特徴を持っています。
- サービスワーカーを利用して、ネットワークリクエストをインターセプトし、モックレスポンスを返す
- REST API や GraphQL API の両方に対応
- 柔軟なモックハンドラの定義が可能
- 開発環境とテスト環境の両方で利用可能
API モックは json-server などでモックサーバを立てる方法や、HTTP クライアントレベルで差し替える方法もありますが、MSW は特に以下の点で優れています。
- 実際のネットワークリクエストをインターセプトするため、アプリケーションコードを変更せずにモックが可能
- ブラウザと Node.js の両方で動作するため、開発環境とテスト環境で同じモックコードを共有しやすい
- 柔軟なモックレスポンスの定義が可能で、テストケースごとにレスポンスを変えるのが簡単
注: 有志のコミュニティによる valendres/msw-playwright が、Playwright 環境で MSW を簡単に利用するためのラッパーライブラリとして提供されており、Playwright と MSW の統合はこのライブラリが前提となります。
コード例
以下に、Playwright と MSW を組み合わせたインテグレーションテストのコード例を示します。
コードの全体は GitHub リポジトリ にありますので、もし興味があればそちらもご覧ください。
以下には TODO アプリケーションを例に、
- MSW ハンドラ定義
- Playwright フィクスチャでの統合
- 正常系 / 異常系のテスト例
を紹介します。
1. MSW ハンドラ定義
API の各エンドポイントに対するモックレスポンスを handlers 配列として定義しています。
以下のコード例では、TODO 一覧取得、TODO 追加、TODO 更新、TODO 削除の各エンドポイントに対するモックレスポンスを定義しています。
// mocks/handlers.js
import { rest } from 'msw'
import todosResponse from '@/mocks/responses/todos.json'
export const handlers = [
// 一覧取得
rest.get(`${BASE_URL}/todos`, (req, res, ctx) => {
const status = req.url.searchParams.get('status')
const filtered =
status === 'active'
? todosResponse.filter((t) => !t.completed)
: status === 'completed'
? todosResponse.filter((t) => t.completed)
: todosResponse
return res(ctx.status(200), ctx.json(filtered))
}),
// 作成
rest.post(`${BASE_URL}/todos`, async (req, res, ctx) => {
const body = req.body ? await req.json() : {}
return res(ctx.status(200), ctx.json(body))
}),
// 更新
rest.patch(`${BASE_URL}/todos/:id`, async (req, res, ctx) => {
const body = req.body ? await req.json() : {}
return res(ctx.status(200), ctx.json(body))
}),
// 削除
rest.delete(`${BASE_URL}/todos/:id`, (req, res, ctx) => {
return res(ctx.status(200), ctx.json({}))
}),
]
2. Playwright フィクスチャでの統合
MSW ハンドラを Playwright のフィクスチャとして組み込み、テストごとに MSW ワーカーが起動・停止するように設定します。
フィクスチャとは、テストケース間で共通して使用するテストの前提条件を分離して管理し、それをテストケースに注入することで再利用を可能にする仕組みです。
Playwright では、test.extend を使用してカスタムフィクスチャを定義できます。
詳しくは Playwright 公式ドキュメント を参照してください。
以下のコード例では、MSW のワーカーに加えて、ページオブジェクトのフィクスチャも定義しています。
ページオブジェクトパターンについてはここでは詳述しませんが、テストコードの可読性や保守性を向上させるための一般的な設計パターンです。
// tests/fixtures.js
import { test as baseTest, expect } from '@playwright/test'
import { rest } from 'msw'
import { createWorkerFixture } from 'playwright-msw'
import { handlers } from '@/mocks/handlers'
import { LoginPage } from '@/tests/pageObjects/loginPage'
import { TopPage } from '@/tests/pageObjects/topPage'
const defaultAuthState = { token: 'mock-token', user: { id: 1, name: 'Guest User' } }
const test = baseTest.extend({
worker: createWorkerFixture(handlers),
rest,
topPage: async ({ page }, use) => { await use(new TopPage(page)) },
})
export { test, expect }
3. テストコード例
以下に、正常系と異常系のテストコード例を示します。
正常系の例
以下のコード例では、「ページにアクセスし、タイトルを入力し、追加ボタンを押す」という一連のユーザーによる操作を再現し、その結果正しいリクエストが送信されることと、画面が正しく表示されることを検証しています。
// tests/index.spec.js
test('タイトルを入力して追加ボタンを押すと TODO が追加されること', async ({ topPage }) => {
await topPage.visit()
const requestPromise = topPage.page.waitForRequest(
(req) => req.method() === 'POST' && req.url().includes('/api/todos'),
)
await topPage.locators.input.fill('新しいタスク')
await topPage.locators.submit.click()
const request = await requestPromise
expect(request.postDataJSON()).toMatchObject({ title: '新しいタスク' })
await topPage.assertOnPage()
})
異常系の例
以下のコード例では、上記の正常系テストと同様の操作を行いますが、追加ボタンをクリックした際に、API から 500 エラーが返ってきた場合に、画面が正しくエラーメッセージを表示することを検証しています。
worker.use を使用して、MSW ハンドラで定義したレスポンスを上書きし、このテストケース内では特定のレスポンスを返すようにしています。
// tests/index.spec.js
test('TODO 追加時にサーバーエラーが発生した場合、エラーメッセージが表示されること', async ({ topPage, rest, worker, page }) => {
await worker.use(
rest.post('http://localhost:8080/api/todos', (_req, res, ctx) => res(ctx.status(500), ctx.json({ message: 'TODO の追加に失敗しました' }))),
)
await topPage.visit()
const alertPromise = page.waitForEvent('dialog', (d) => d.type() === 'alert')
await topPage.locators.input.fill('エラーテスト')
await topPage.locators.submit.click()
const alertDialog = await alertPromise
expect(alertDialog.message()).toBe('TODO の追加に失敗しました')
await alertDialog.dismiss()
await topPage.assertOnPage()
})
まとめ
Web フロントエンドの自動テストにおける、実行速度や安定性の課題を抑えつつ、本来の目的である「ユーザーに近い視点でアプリケーションの動作を検証する」ことを実現する構成として、Playwright と MSW を組み合わせたインテグレーションテストの方法を紹介しました。
まだまだ私の周りでは自動テストが十分に普及していない状況ですが、少しずつでも自動テストの重要性が認識され、品質向上に寄与できればと思っています。
参考資料・リンク
- Kent C. Dodds - Testing JavaScript: Learn the smart, efficient way to test any JavaScript application.: https://testingjavascript.com/
- MSW: https://mswjs.io/
- Playwright: https://playwright.dev/
- valendres/msw-playwright: https://github.com/valendres/playwright-msw