📑

Vitest+mswを使ってAzureStaticWebAppのReact+SWRアプリをテストする

2024/01/15に公開

今回はAzure Static Web Apps用に、Vite+React+SWRで作ったフロントエンドに、Vitest+mswでテストを書いていきます。

テスト対象は、下記の『AzureStaticWebAppをReact+AzureFunctions(Python)で作って、ローカルで動かしてみる』で作成したReactアプリになります。

  1. 基本編
    1. AzureStaticWebAppをReact+AzureFunctions(Python)で作って、ローカルで動かしてみる
  2. Azure Functions編
    1. Azure Functions(Python版)の試験をpytestで行う
    2. hatchでAzure Functions(Python版)のLintチェックをしながらリファクタリングする
    3. behaveを使ってAzure Functions(Python版)でBDD(振る舞い駆動開発)を始める
    4. Azure FunctionsでBluePrint対応を行う

このアプリは、Viteで作成された react-swc-ts アプリを元に、Azure FunctionsのAPIレスポンスを表示させたアプリです。

ブラウザで表示した場合下記のような形になっています。

現段階の表示イメージ

基本的な手順はVitestの公式を踏襲する形ですが、前段階で実装したAzure FunctionsのAPIをモックするために、mswを使ってテストを記述していきます。

今回はVite+React編のため、作業ディレクトリは下記になります。

~/devel/sandbox_vite

1. 環境構築

1.1. Vitestを実行するための環境構築

まずはVitestを実行する環境を作っていきます。

  1. 依存ライブラリをインストールします
    実行コマンド
    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.
    
  2. 次に 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",
    
  3. 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']
    +     },
    +   },
      })
    
    
  4. .gitignore にテスト成果物を無視する設定を追加します
    .gitignore
    
      〜〜〜〜〜(前略)〜〜〜〜〜
    
      *.njsproj
      *.sln
      *.sw?
    
    + coverage
    + 
    
  5. 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" }]
      }
    
  6. テスト用の各種ユーティリティを格納するフォルダを作成します
    実行コマンド
    mkdir -p src/test/
    
  7. テスト時のセットアップの際に実行する src/test/setup.ts ファイルを下記の内容で作成します
    src/test/setup.ts
    import '@testing-library/jest-dom'
    
    
  8. テスト用の各種設定を 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のインストールと設定をしていきます。

  1. 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.
    
  2. テストのセットアップファイル(src/test/setup.ts)にmswの設定を追加します
    src/test/setup.ts
      import '@testing-library/jest-dom'
    + import { server } from './mocks/server'
    + 
    + beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
    + afterAll(() => server.close())
    + afterEach(() => server.resetHandlers())
    + 
    
  3. APIのモック設定を入れるためのフォルダを作成します
    実行コマンド
    mkdir -p src/test/mocks/
    
  4. モックサーバーの初期化を行うコードを src/test/mocks/server.ts に作成します
    src/test/mocks/server.ts
    import { setupServer } from 'msw/node'
    import { handlers } from './handlers'
    
    // This configures a Service Worker with the given request handlers.
    export const server = setupServer(...handlers)
    
    
  5. モックサーバーのリクエストとレスポンスを設定するハンドラー用ファイル(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 = [
    ]
    
    
  6. テスト用設定ファイルの最後に下記の内容を追記します
    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';
    + 
    
  7. ここまでの内容を記録します
    実行コマンド
    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 !"}

こうすることで、もしモックに失敗していたとしても、そのことに気づくことができます。

  1. エディタで 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に対するテストを書く

次に各々の仕様を確認するテストを記述していきます。

  1. 新たにファイルsrc/App.test.tsx を作成し、下記の内容を記述します
    src/App.test.tsx
    import 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のモックとテストコードの作成ができたので、実行していきます。

  1. 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 の仕様を確認できました。

まとめ

今回はAzure Static Web Apps用に、Vite+React+SWRで作ったフロントエンドに、Vitestでテストを行うところまで実装できました。
さらにその上でバックエンドのAPI部分はmswでモック化し、フロントエンド部のみでテストを実行するところまで完了しました。
これでAPIさえフロントエンドとバックエンドの間で合わせれば、各々独立して開発を進めることができるようになりました。

次回は、APIがエラーを返したり、タイムアウトするケースをmswを使って書いていくか、ReactRouterを使ったページ遷移を書いていくかしていくと思います。

参考

Discussion