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'
import { Button } from '../../components/atoms/Button'
import { LoginBox, LoginTitle } from '../../../style/pages/Login'
import { Form, FormLabel, FormInput, FormButton, FormValidButton, FormValidTxt } from '../../../style/components/block/Form'
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)
要件としては
流れになります。
レスポンスデータを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
以下ファイルで定義している内容としては
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,
}),
}));
〜〜省略〜〜
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