Vitest+mswを使ってAzureStaticWebAppのReact+SWRアプリをテストする
今回はAzure Static Web Apps用に、Vite+React+SWRで作ったフロントエンドに、Vitest+mswでテストを書いていきます。
テスト対象は、下記の『AzureStaticWebAppをReact+AzureFunctions(Python)で作って、ローカルで動かしてみる』で作成したReactアプリになります。
- 基本編
- Azure Functions編
このアプリは、Viteで作成された react-swc-ts
アプリを元に、Azure FunctionsのAPIレスポンスを表示させたアプリです。
ブラウザで表示した場合下記のような形になっています。
基本的な手順はVitestの公式を踏襲する形ですが、前段階で実装したAzure FunctionsのAPIをモックするために、mswを使ってテストを記述していきます。
今回はVite+React編のため、作業ディレクトリは下記になります。
~/devel/sandbox_vite
1. 環境構築
1.1. Vitestを実行するための環境構築
まずはVitestを実行する環境を作っていきます。
- 依存ライブラリをインストールします実行コマンド
yarn add --dev jsdom \ @testing-library/react @testing-library/user-event @testing-library/jest-dom \ vitest @vitest/coverage-v8 @vitest/ui
出力結果yarn add v1.22.21 [1/4] 🔍 Resolving packages... [2/4] 🚚 Fetching packages... [3/4] 🔗 Linking dependencies... warning " > @testing-library/user-event@14.5.2" has unmet peer dependency "@testing-library/dom@>=7.21.4". [4/4] 🔨 Building fresh packages... 〜〜〜〜〜(中略)〜〜〜〜〜 ├─ which-typed-array@1.1.13 ├─ why-is-node-running@2.2.2 ├─ ws@8.16.0 ├─ xmlchars@2.2.0 └─ yocto-queue@1.0.0 ✨ Done in 15.66s.
- 次に
package.json
をエディタで開いて、下記のようにVitestを実行するためのコマンドを設定しますpackage.json"version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest", + "test:ui": "vitest --ui", + "coverage": "vitest run --coverage" }, "dependencies": { "axios": "^1.6.2", "react": "^18.2.0",
- Viteの設定ファイルである
vite.config.ts
にテスト用の設定を追加しますvite.config.ts+ /// <reference types="vitest" /> + /// <reference types="vite/client" /> + import { defineConfig } from 'vite' + import { configDefaults } from 'vitest/config' import react from '@vitejs/plugin-react-swc' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: './src/test/setup.ts', + // you might want to disable it, if you don't have tests that rely on CSS + // since parsing CSS is slow + css: true, + coverage: { + provider: 'v8', + exclude: [...configDefaults.coverage.exclude, 'src/test', 'src/main.tsx'] + }, + }, })
-
.gitignore
にテスト成果物を無視する設定を追加します.gitignore〜〜〜〜〜(前略)〜〜〜〜〜 *.njsproj *.sln *.sw? + coverage +
- Vitestの頻出メソッドをあらかじめimportする設定を追記しますtsconfig.json
{ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, + "types": ["vitest/globals"], + /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] }
- テスト用の各種ユーティリティを格納するフォルダを作成します実行コマンド
mkdir -p src/test/
- テスト時のセットアップの際に実行する
src/test/setup.ts
ファイルを下記の内容で作成しますsrc/test/setup.tsimport '@testing-library/jest-dom'
- テスト用の各種設定を
src/test/test-utils.tsx
に書き込んでいきますsrc/test/test-utils.tsx/* eslint-disable react-refresh/only-export-components */ import { cleanup, render } from '@testing-library/react' import { afterEach } from 'vitest' afterEach(() => { cleanup() }) function customRender(ui: React.ReactElement, options = {}) { return render(ui, { // wrap provider(s) here if needed wrapper: ({ children }) => children, ...options, }) } export * from '@testing-library/react' export { default as userEvent } from '@testing-library/user-event' // override render export export { customRender as render }
ここまでで一般的なVitest用の設定は完了です。
1.2. APIをモックするmswを使うための環境構築
今回のVite+Reactは、Azure Functionsと連携するフロントエンドアプリです。
そのためテスト時にAzure Functionsをモックしてあげる必要があります。
VitestではAPIのモック機能であるmswを使えるので、合わせてmswのインストールと設定をしていきます。
- mswをインストールします実行コマンド
yarn add --dev msw
出力結果yarn add v1.22.21 [1/4] 🔍 Resolving packages... [2/4] 🚚 Fetching packages... [3/4] 🔗 Linking dependencies... warning " > @testing-library/user-event@14.5.2" has unmet peer dependency "@testing-library/dom@>=7.21.4". [4/4] 🔨 Building fresh packages... 〜〜〜〜〜(中略)〜〜〜〜〜 ├─ run-async@2.4.1 ├─ strict-event-emitter@0.5.1 └─ through@2.3.8 ✨ Done in 5.65s.
- テストのセットアップファイル(
src/test/setup.ts
)にmswの設定を追加しますsrc/test/setup.tsimport '@testing-library/jest-dom' + import { server } from './mocks/server' + + beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) + afterAll(() => server.close()) + afterEach(() => server.resetHandlers()) +
- APIのモック設定を入れるためのフォルダを作成します実行コマンド
mkdir -p src/test/mocks/
- モックサーバーの初期化を行うコードを
src/test/mocks/server.ts
に作成しますsrc/test/mocks/server.tsimport { setupServer } from 'msw/node' import { handlers } from './handlers' // This configures a Service Worker with the given request handlers. export const server = setupServer(...handlers)
- モックサーバーのリクエストとレスポンスを設定するハンドラー用ファイル(
src/test/mocks/handlers.ts
)を下記の内容で作成しますsrc/test/mocks/handlers.ts// Define handlers that catch the corresponding requests and returns the mock data. export const handlers = [ ]
- テスト用設定ファイルの最後に下記の内容を追記しますsrc/test/test-utils.tsx
/* eslint-disable react-refresh/only-export-components */ import { cleanup, render } from '@testing-library/react' import { afterEach } from 'vitest' afterEach(() => { cleanup() }) function customRender(ui: React.ReactElement, options = {}) { return render(ui, { // wrap provider(s) here if needed wrapper: ({ children }) => children, ...options, }) } export * from '@testing-library/react' export { default as userEvent } from '@testing-library/ user-event' // override render export export { customRender as render } + export { server } from './mocks/server' + export { HttpResponse, http } from 'msw' + export { SWRConfig } from 'swr'; +
- ここまでの内容を記録します実行コマンド
git commit -m "Vitest+mswの環境を構築"
出力結果[main 0867873] Vitest+mswの環境を構築 8 files changed, 1754 insertions(+), 26 deletions(-) create mode 100644 src/test/mocks/handlers.ts create mode 100644 src/test/mocks/server.ts create mode 100644 src/test/setup.ts create mode 100644 src/test/test-utils.tsx
ここまででmswの設定が完了しました。
2. 基本的なテストを作成する
いよいよテストを作成していきます。
現段階では下記のようなページが表示されます。
これは下記のような仕様です。
- 見出し1に"Vite + React"と表示されている
- APIレスポンスに含まれているmessageの値が表示されている
- ロード中は"loading..."と表示されている
- "count is 0"と表示されているボタンをクリックしたら、"count is 1"に変化する
これらに対してテストを記述していきます。
2.1. APIサーバーのモック設定を登録する
まずAzure FunctionsのAPIをモックするための設定を記述していきます。
Azure Functionsでは下記のようなメッセージを返す仕様でした。
{"message": "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."}
このままのメッセージをモックに組み込んでもいいのですが、今回はちゃんとモックできていることを確認するために、下記のようなレスポンスを返すように設定します。
{"message": "Hello from msw !"}
こうすることで、もしモックに失敗していたとしても、そのことに気づくことができます。
- エディタで
src/test/mocks/handlers.ts
を開いて、下記のように追記しますsrc/test/mocks/handlers.ts+ import { HttpResponse, http } from 'msw' + + // Mock Data + const resData = { message: 'Hello from msw !' } + // Define handlers that catch the corresponding requests and returns the mock data. export const handlers = [ + http.get('/api/MyHttpTrigger', () => { + return HttpResponse.json(resData, { status: 200 }) + }), ]
2.2. 現行のApp.tsxに対するテストを書く
次に各々の仕様を確認するテストを記述していきます。
- 新たにファイル
src/App.test.tsx
を作成し、下記の内容を記述しますsrc/App.test.tsximport App from './App' import { render, screen, waitFor, SWRConfig, userEvent } from './test/test-utils' describe(App, () => { it('見出し1に"Vite + React"と表示されている', async () => { render(<SWRConfig value={{ provider: () => new Map() }}><App /></SWRConfig>) await waitFor(() => { expect(screen.getByRole('heading', { name: 'Vite + React', level: 1 })).toBeDefined() }) }) it('APIレスポンスに含まれているmessageの値が表示されている', async () => { render(<SWRConfig value={{ provider: () => new Map() }}><App /></SWRConfig>) await waitFor(() => { expect(screen.getByText('Hello from msw !')).toBeInTheDocument() }) }) it('ロード中は"loading..."と表示されている', () => { render(<SWRConfig value={{ provider: () => new Map() }}><App /></SWRConfig>) expect(screen.getByText('loading...')).toBeInTheDocument() }) it('"count is 0"と表示されているボタンをクリックしたら、"count is 1"に変化する', async () => { render(<SWRConfig value={{ provider: () => new Map() }}><App /></SWRConfig>) await waitFor(() => { const button = screen.getByRole('button', { name: 'count is 0' }) expect(button).toBeDefined() userEvent.click(button) waitFor(() => { expect(screen.getByText('count is 1')).toBeInTheDocument() }) }) }) })
2.3. テストを実行
APIのモックとテストコードの作成ができたので、実行していきます。
-
yarn test
コマンドでテストを実行します実行コマンドyarn test
出力結果yarn run v1.22.21 $ vitest DEV v1.1.3 ~/devel/sandbox_vite ✓ src/App.test.tsx (4) ✓ App (4) ✓ 見出し1に"Vite + React"と表示されている ✓ APIレスポンスに含まれているmessageの値が表示されている ✓ ロード中は"loading..."と表示されている ✓ "count is 0"と表示されているボタンをクリックしたら、"count is 1"に変化する Test Files 1 passed (1) Tests 4 passed (4) Start at 23:39:02 Duration 1.12s (transform 184ms, setup 193ms, collect 317ms, tests 107ms, environment 350ms, prepare 52ms) PASS Waiting for file changes... press h to show help, press q to quit
yarnからvitestを実行し、テストが完了しました。
これで App.tsx
の仕様を確認できました。
2.4. ここまでの作業を記録
ここまでの作業を記録しておきます。
- ターミナルで下記のコマンドを入力します実行コマンド
git add . git commit -m "Appのテストを作成"
出力結果[main edbf96c] Appのテストを作成 2 files changed, 49 insertions(+) create mode 100644 src/App.test.tsx
まとめ
今回はAzure Static Web Apps用に、Vite+React+SWRで作ったフロントエンドに、Vitestでテストを行うところまで実装できました。
さらにその上でバックエンドのAPI部分はmswでモック化し、フロントエンド部のみでテストを実行するところまで完了しました。
これでAPIさえフロントエンドとバックエンドの間で合わせれば、各々独立して開発を進めることができるようになりました。
次回は、APIがエラーを返したり、タイムアウトするケースをmswを使って書いていくか、ReactRouterを使ったページ遷移を書いていくかしていくと思います。
Discussion