Wiz テックブログ

Wizは、最新のIoTやICTサービスをお客様に届ける「ITの総合商社」です。

Mock Service Workerを使ってOpenAPIに寄り添ったテストを行う。

Tech事業部プロダクトチームの仲本です。

10月から、フロントエンドチームからプロダクトチームになりました。

今回は、現在開発しているプロダクトに、テストを導入するにあたって、Mock Service Workerを導入し、OpenAPIと組みわせてMockAPIを作成しテストを作成しました。

今回の記事は導入記録/所感的なものを書いています。

テストを導入する経緯

  • フロントエンドのテストがまずできていなかったこと
  • 社内業務案件で、リリースのたびにチームで手作業でテストを行っていたこと
  • 今後規模が大きくなると、手作業でのテストが辛くなる

こういった経験のもと、テストを導入し安全性を確保したいということになりました。

jest.mockによるmock化がかなり多くなる話

実際にjestを導入して、テストを書いていくとmock化しないと通らないテストがあること気づきました。

apiを実行する際のfetchの処理などもmock化させると、実際の動きと遠くなるなるのではないかという懸念があり調べているとMock Service Workerに出会いました。

Mock Service Workerとは

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

ユーザーログイン画面のテスト実装

今回はログイン画面のテスト実装していきます。

Login画面で表示するコンポーネントが以下になります。

import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { jsx } from '@emotion/react'
import { useForm, UseFormReturn } from "react-hook-form"
import { BrowserRouter as Router, useHistory } from 'react-router-dom'
import { hot } from 'react-hot-loader'

import {
  fetchAsyncLogin,
  selectError,
} from '../../../stores/slices/authSlice'


// component
import { Button } from '../../components/atoms/Button'

// style
import { LoginBox, LoginTitle } from '../../../style/pages/Login'
import { Form, FormLabel, FormInput, FormButton, FormValidButton, FormValidTxt } from '../../../style/components/block/Form'


//type
import { AppDispatch } from '../../../stores';
import { LoginFormInput } from '../../../../types/user';

const Login = () => {
  const methods: UseFormReturn<LoginFormInput> = useForm<LoginFormInput>();
  const { register, handleSubmit, formState: { errors }, reset } = methods
  const dispatch: AppDispatch = useDispatch()
  const history = useHistory()

  const onSubmit = async (data: LoginFormInput) => {
    const result = await dispatch(fetchAsyncLogin(data))
    if (fetchAsyncLogin.fulfilled.match(result)) {
      if (result.payload.status == 200) {
        history.push('/')
      }
    }
    reset()
  }

  return (
    <div>
      <div css={LoginBox}>
        <form css={Form} onSubmit={handleSubmit(onSubmit)}>
          <label css={FormLabel}>
            メールアドレス
            <input
              autoComplete="email"
              type="email"
              aria-invalid={errors.mailAddress ? "true" : "false"}
              {...register("mailAddress", { required: true, pattern: /^([a-zA-Z0-9])+([a-zA-Z0-9\._+-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$/ })}
              placeholder="メールアドレスを入力してください"
              css={FormInput}
            />
          </label>
          <label css={FormLabel}>
            パスワード
            <input
              autoComplete="password"
              type="password"
              {...register("passWord", { required: true, pattern: /^[a-z\d]{1,100}$/i })} aria-invalid={errors.passWord ? "true" : "false"} placeholder="パスワードを入力してください"
              css={FormInput}
            />
          </label>
          <Button name="ログイン" cssStyle={FormButton} onClick={handleSubmit(onSubmit)} dataTestId="test-2-submit-btn"  />
        </form>
      </div>
    </div>
  )
}
export default hot(module)(Login)

要件としては

  • メールアドレスが入力できる

  • パスワードが入力できる

  • ログインボタンを押すとログイン用APIにPOSTする

流れになります。

レスポンスデータをOpenAPI定義から取得する

現在携わっているプロジェクトでは、OpenAPIを使用してAPIのやりとり/認識合わせを行なっています。 OpenAPI仕様が記載されたjsonファイルを使用し、Mock Service Workerの設定を行います。

そうすることで、より使用されているAPIを忠実に再現できることや、もし仕様の変更があった際もしっかり新しい情報の入ったOpenAPIを取り込めていたら気付けるのではないかと思い使用しました。

以下がOpenAPIで定義している内容になります。

// openapi.json

"paths": {
  "/api/login": {
// ~~省略~~
  "responses": {
    "200": {
      "description": "HTTP OK",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
// ~~省略~~
            "examples": { // <= 今回レスポンスで使用するデータ
              "default": {
                "value": {
                  "status": 200,
                  "response_time": 1.1535649299621582,
                  "message": "処理が正常終了しました。",
                    "data": {
                      "login_id": "test@hoge.co.jp",
                      "token": "123|njGYLOG9EuZuIrSv83dUvnIWzFLbo6Ri5mUOLm4q",
                    }
                 }
              }
            }

上記のjsonファイルをimportし、examplesを以下のファイルでimportします。

この設定をすることにより、jsonファイルに変更があった際に変更に対応してくれます。

import schema from '../../openapi.json'

const components = {
  LoginUser: schema.paths['/api/login'].post.responses[200].content['application/json'].examples.default.value,
}
export default components 

Mock Service Workerでhandlerを設定

次にmockを作成していきます。

mswからrestをimportしgetやpostの設定を行います。

今回はpostの設定を行います。

参考: mswjs.io

以下ファイルで定義している内容としては

  • login_idがtest@hoge.co.jpかつpasswordがtest1234@

  • 上の条件を満たしていた時ステータス200と、先ほど定義したexamplesを返す設定をしています。

import { rest } from 'msw'
import components from './components'

const handlers = [
  rest.post<Record<string, any>>('http://localhost:3000/api/login', (req, res, ctx) => {
    const { login_id, password } = req.body
    if (login_id === 'test@hoge.co.jp' && password === 'test1234@') {
      return res(
        ctx.status(200),
        ctx.json(components.LoginUser)
      )
    } else {
      return res(
        ctx.status(403),
        ctx.json({
          error: 'error: invalid username or password'
        })
      )
    }
  }),
]

export { handlers } 

jest.setup.js

jest.setup.jsというファイルで、先程設定をしたmockを動かすためのserverの設定と

node環境だと、window.fetchが使えないので node-fetchをinstallして設定を行います。

github.com

import server from './src/test/lib/msw/server'
import { cleanup } from '@testing-library/react'
import fetch from "node-fetch"

beforeAll(() => server.listen())
afterEach(async () => {
  server.resetHandlers()
})
afterAll(() => server.close())

if (!globalThis.fetch) {
  globalThis.fetch = fetch
}

実際のテストコード

import React from 'react';
import '@testing-library/jest-dom/extend-expect'
import { fireEvent, getAllByText, render, screen, waitFor } from '@testing-library/react'
import { Provider } from 'react-redux';
import { act } from 'react-dom/test-utils';

import store from '../tsx/stores';

import Login from '../tsx/views/pages/login/Login'


const LoginComponent =
  <Provider store={store}>
    <Login />
  </Provider>

const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
  useHistory: () => ({
    push: mockHistoryPush, // pushメソッドをダミー関数で上書きする。
  }),
}));

〜〜省略〜〜
describe('test 2 ログイン処理', () => {
  it('ログイン確認', async () => {
    const { getByTestId } = render(LoginComponent)

    await act(async () => {
      fireEvent.change(screen.getByLabelText(/メールアドレス/i), {
        target: { value: 'test@hoge.co.jp' },
      });

      fireEvent.change(screen.getByLabelText(/パスワード/i), {
        target: { value: 'test1234@' },
      })
    });

    await act(async () => {
      fireEvent.submit(getByTestId('test-2-submit-btn'))
    });

    await waitFor(() => {
      expect(mockHistoryPush).toBeCalledWith('/');
    })
  });
}) 

ログインボタンを押した時のAPI処理のみのテストを表示しています。

ログインボタンを押すと先程handlerで設定したMockを実行するようになっています。

以上がユーザーログイン画面のテスト実装でした。

今後考えていきたいこと

  • どの範囲をテストしていくべきかを明確にする
  • 別のプロジェクトに導入するために、どういった開発手法がいいかを考えていく
  • まだ導入したてなので色々実装していきながらベストを探していきたい

上記をまず、明確にできるように頑張っていきたいと思います。

参考記事

OpenAPI定義をmswに活用してお手軽モック

Mock Service Worker で jest.mock を使わず非同期リクエストのテストを書く

最後に

Wizではエンジニアを募集しております。
興味のある方、ぜひご覧下さい!

careers.012grp.co.jp