Open9

Vue.jsのテスト環境まわり

hirotakahirotaka

jestを使えるようにします。

Vue CLIがグローバルではいっていて、すでにあるVue.jsのプロジェクトに追加する場合。

vue add unit-jest

package.jsonscriptsに追加します。

package.json
   "scripts": {
     "serve": "vue-cli-service serve",
     "build": "vue-cli-service build",
+    "test:unit": "vue-cli-service test:unit",
   },

npm runで実行します。

npm run test:unit

デフォルトだと下記のファイルが対象になります。

  • tests/unitディレクトリ以下にある、.spec.(js|jsx|ts|tsx)で終わるファイル名
  • __tests__というディレクトリ名にある、拡張子がjs(x)/ts(x)のファイル
hirotakahirotaka

Mock Service Worker

Mock Service Workerは、Service WorkerのAPIを使って実際のリクエストをインターセプトしてAPIをモックするライブラリです。

jestで外部のAPIを呼ぶようなテストを書くときに便利です。

セットアップ

パッケージをインストールします。

npm install msw --save-dev

モック定義

リクエストを扱うためのハンドラー、ブラウザやサーバ特有のセットアップなどモックの動作を定義します。どのように整理をしてもいいのですが、ここではそれらを一つにまとめておくためのディレクトリを作成します。

mkdir src/mocks

ハンドラーを定義するためにファイルを作成します。

touch src/mocks/handlers.js
hirotakahirotaka

REST APIをモック

インポート

src/mocks/handlers.jsファイルでは、REST APIをモックするために必要なものをインポートします。これらはライブラリで公開されているrest名前空間の下にまとめられています。

src/mocks/handlers.js
import { rest } from 'msw'
hirotakahirotaka

リクエスト・ハンドラー

REST APIのリクエストを処理するには、メソッド、パス、そしてモックされたレスポンスを返す関数を指定する必要があります。

ここでは、ユーザのための基本的なログインのフローをモックします。このフローでは、2つのリクエストを処理します。

  • POST /login:ユーザーのログインを許可します。
  • GET /user:ログインしたユーザの情報を返します。

rest[METHOD]を呼び出し、リクエストパスを指定してリクエストハンドラを作成します。

src/mocks/handlers.js
import { rest } from 'msw'

export const handlers = [
  // Handles a POST /login request
  rest.post('/login', null),

  // Handles a GET /user request
  rest.get('/user', null),
]
hirotakahirotaka

レスポンス・レゾルバ

傍受されたリクエストに応答するには、レスポンス・リゾルバ関数を使って、モックされたレスポンスを指定する必要があります。

レスポンス・リゾルバは、次のような引数を受け取る関数です。

  • req:一致するリクエストに関する情報。
  • res:モックされたレスポンスを作成するための機能的なユーティリティ。
  • ctx:モックアップされたレスポンスのステータスコード、ヘッダー、ボディなどを設定するための関数群。

先に定義したリクエスト・ハンドラにレスポンス・リゾルバを提供する。

src/mocks/handlers.js
import { rest } from 'msw'
export const handlers = [
  rest.post('/login', (req, res, ctx) => {
    // Persist user's authentication in the session
    sessionStorage.setItem('is-authenticated', 'true')
    return res(
      // Respond with a 200 status code
      ctx.status(200),
    )
  }),
  rest.get('/user', (req, res, ctx) => {
    // Check if the user is authenticated in this session
    const isAuthenticated = sessionStorage.getItem('is-authenticated')
    if (!isAuthenticated) {
      // If not authenticated, respond with a 403 error
      return res(
        ctx.status(403),
        ctx.json({
          errorMessage: 'Not authorized',
        }),
      )
    }
    // If authenticated, return a mocked user details
    return res(
      ctx.status(200),
      ctx.json({
        username: 'admin',
      }),
    )
  }),
]

sessionStorage、localStorage、IndexedDBなどを利用して、より複雑なAPIシナリオやユーザーのインタラクションを処理します。

hirotakahirotaka

統合

同じリクエストハンドラをブラウザ環境とNode環境で共有することができます。サービスワーカーはNodeで実行できないので、統合のやり方は環境によって異なります。

hirotakahirotaka

ブラウザ

セットアップ

Mock Service Workerは、リクエストの傍受を行うService Workerを登録することで、クライアントサイドで動作します。しかし、ワーカーのコードを自分で書く必要はなく、ライブラリから配布されているワーカーのファイルをコピーすればよいのです。Mock Service Workerは、そのための専用のCLIを提供しています。

Mock Service WorkerのCLIのinitコマンドを実行します。

$ npx msw init public/ --save

ここでは、--saveオプションを使って、package.jsonに指定したワーカーディレクトリ(「public」)を保存していることに注意してください。これにより、将来的にmswパッケージを更新する際に、ワーカースクリプトの更新が自動的に適用されます。

ワーカーの設定

モックの定義ディレクトリ(src/mocks)に、Service Workerを設定・起動するためのファイルを作成しましょう。

src/mocks/browser.jsファイルを作成します。

touch src/mocks/browser.js

browser.jsファイルでは、先に定義したリクエストハンドラを持つWorkerインスタンスを作成します。

mswパッケージからsetupWorker関数をインポートし、先に定義したリクエスト・ハンドラを持つワーカー・インスタンスを作成します。

src/mocks/browser.js
import { setupWorker } from 'msw'
import { handlers } from './handlers'

// This configures a Service Worker with the given request handlers.
export const worker = setupWorker(...handlers)

ワーカーを起動

モックの定義をランタイム中に実行するためには、アプリケーションのコードにインポートする必要があります。しかし、モックは開発向けの技術であるため、src/mocks/browser.jsファイルを現在の環境に応じて条件付きでインポートすることになります。

以下の例にしたがって、src/mocks/browser.jsファイルを条件付きでインポートします。

src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
if (process.env.NODE_ENV === 'development') {
  const { worker } = require('./mocks/browser')
  worker.start()
}
ReactDOM.render(<App />, document.getElementById('root'))

ワーカーの起動は非同期に行われるため、マウント時にリクエストを行うアプリケーションとの間に競合状態が生じる可能性があります。そのような場合は、Deferred mountingレシピを参照して、ワーカーの準備ができたときにアプリケーションのマウントを強制してください。

検証と検査

モック定義をインポートした後、ブラウザのコンソールにMock Service Workerからの起動成功メッセージが表示されるはずです。

[MSW] Mocking enabled

先に定義したハンドラにマッチするリクエストは、すべてインターセプトされ、モック化されます。

hirotakahirotaka

Node

NodeにおけるMock Service Workerの最も一般的な使用法の一つは、統合テストにリクエスト・ハンドラを利用することです。ここでは、テストランナーとしてJestを使用します。同じ原理で、Nodeのどのプロセスにもモックを統合することができます。

サーバを設定

モックの定義ディレクトリ(src/mocks)に、リクエストモックのサーバーを設定するファイルを作成しましょう。

src/mocks/server.jsファイルを作成します。

src/mocks/server.js
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
// This configures a request mocking server with the given request handlers.
export const server = setupServer(...handlers)

セットアップ

セットアップモジュールを作成し、jest.config.jssetupFilesAfterEnvオプションに設定してください。

テスト用のセットアップファイルjest.setup.jsを作成します。

touch jest.setup.js
src/setupTests.js
import { server } from './mocks/server.js'
// Establish API mocking before all tests.
beforeAll(() => server.listen())
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers())
// Clean up after the tests are finished.
afterAll(() => server.close())

jest.config.jsファイルに追加します。

jest.config.js
module.exports = {
  preset: '@vue/cli-plugin-unit-jest',
  transform: {
    '^.+\\.vue$': 'vue-jest'
  },
+  setupFilesAfterEnv: ["./jest.setup.js"],
}

テストを実行

APIのモック化はテストの設定で確立されているので、各テストスイートは、ハンドラーに応じてAPIリクエストをインターセプトし、モック化するための特別な調整は必要ありません。

test/Login.test.js
test('allows user to log in', async () => {
  // Render components, perform requests, receive mocked responses.
})

直接の使用方法

Mock Service WorkerのsetupServer APIは、任意のNodeJSアプリケーションで使用することができます(例えば、Expressサーバの開発やテストを行う場合など)。

JestのjsdomのようなDOMライクな環境がない場合、NodeJSでは絶対的なリクエストURLを使用しなければならないことを覚えておいてください。これは、リクエスト・ハンドラに反映させる必要があります。

const server = setupServer(
  // NOT "/user", nothing to be relative to!
  rest.get('https://api.backend.dev/user', (req, res, ctx) => {
    return res(ctx.json({ firstName: 'John' }))
  }),
)