Movatterモバイル変換


[0]ホーム

URL:


Zenn
ryo_kawamataryo_kawamata
🍊

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

に公開
2021/05/04
1件

Mock Service Worker について色々試したので紹介です。

Mock Service Worker とは?

Mock Service Worker(以下 msw)は、ネットワークレベルで API リクエストをインターセプトして mock のデータを返すためのライブラリです。API リクエストを含む処理のテストや、SPA 開発時の mock サーバーとして利用出来ます。

https://mswjs.io/

以下テストで利用する場合のサンプルコードです。
setupServerでインターセプト用のサーバーを定義し、listen()でインターセプトをスタート、close()でインターセプトをストップします。

!
import{ rest}from"msw"import{ setupServer}from"msw/node";import axiosfrom"axios";// mockサーバーconst mockServer=setupServer(  rest.get('/greeting',(req, res, ctx)=>{returnres(ctx.status(200), ctx.json('Hello'))}))// テスト対象の関数constgreeting=async(name:string)=>{const word=await axios.get('/greeting')return`${word.data}${name}`}describe('greeting',()=>{beforeAll(()=>{// インターセプトスタート    mockServer.listen()})afterAll(()=>{// インターセプトストップ    mockServer.close()})test('挨拶を返す',async()=>{const result=awaitgreeting('ryo')expect(result).toEqual('Hello ryo')// Green})})

なぜネットワークレベルでのmockが必要なの?

なぜjest.mockではなくネットワークレベルのモックが必要かというと、mock のレイヤーが低レイヤーになるほどテストがより安全になるからです。次章で詳細な例を書きますが、非同期リクエストを投げるモジュールをモックしてしまうと、そのモジュール自体のバグをそのテストでは担保できなくなってしまいます。
一般的により安全なテストを書くためには、モックをなるべく使わない or モックのレイヤーを下げることが必要です。

※ モックを使うことでテスト実行のパフォーマンスを向上させるというメリットもあるので、ケースバイケースではあります。

Vueコンポーネントのテストでの利用例

実際のユースケースでありそうなログイン処理についてのテストコードを書いていきます。
テストフレームワークは Jest と、Vue Testing Library を使います。

https://github.com/facebook/jest

https://github.com/testing-library/vue-testing-library

テスト対象のコード

以下ログインフォームのコンポーネントを対象にします。
このコンポーネントの機能は以下です。

  • ユーザー名とパスワードを入力出来る
  • submit ボタンを押すとログイン API をコールする
  • ログインに成功するとHello <username>を表示する
  • ログインに失敗すると Error を表示する
Login.vue
<template><h1v-if="user">    Hello, {{ user.name }}</h1><form@submit.prevent="handleAuth"><labelfor="username">Username:<inputv-model="formData.username"id="username"name="username"/></label><labelfor="password">Password:<inputv-model="formData.password"id="password"name="password"/></label><button>submit</button></form><spanv-if="error"data-testid="error">{{ error }}</span></template><scriptlang="ts">import{ defineComponent, reactive, ref}from"@vue/runtime-core";importaxiosfrom'axios';typeUser={name: string,}exportdefaultdefineComponent({setup(){const formData=reactive({username:'',password:'',})const user= ref<null|User>(null)const error= ref<null| string>(null)consthandleAuth=async()=>{try{const response=await axios.post('/login',{username: formData.username,password: formData.password})        user.value= response.data}catch(e){        error.value= e.response.data.error}}return{      formData,      handleAuth,      user,      error};}});</script>

Jest.mockを使ってaxiosをモックしたコード

まず最初に、jest.mock を使って axios をモックする例です。
jest.mock('axios')で axios のモジュールをモックして、mockResolvedValuemockRejectedValueでモックの戻り値を定義することで正常系と異常系の UI をテストしています。

Login.vue.test
import{ render, screen, fireEvent}from'@testing-library/vue'import Loginfrom"../../components/Login.vue"import axiosfrom'axios'jest.mock('axios')const mockedAxios= axiosas jest.Mocked<typeof axios>describe('Login',()=>{test('ログインが成功した場合にユーザー名を表示する',async()=>{    mockedAxios.post.mockResolvedValue({ data:{ name:'validUsername'}})render(Login)expect(screen.queryByText('Hello, validUsername')).toBeFalsy()await fireEvent.update(screen.getByLabelText(/username/i),'validUser')await fireEvent.update(screen.getByLabelText(/password/i),'validPassword')await fireEvent.click(screen.getByText('submit'))expect(await screen.findByText('Hello, validUsername')).toBeTruthy()expect(screen.queryByTestId('error')).toBeFalsy()})test('ログインが失敗した場合にエラーを表示する',async()=>{    mockedAxios.post.mockRejectedValue({ response:{ data:{ error:'error: invalid username or password'}}})render(Login)expect(screen.queryByText('Hello, validUsername')).toBeFalsy()await fireEvent.update(screen.getByLabelText(/username/i),'invalidUser')await fireEvent.update(screen.getByLabelText(/password/i),'invalidPassword')await fireEvent.click(screen.getByText('submit'))expect(await screen.findByTestId('error')).toBeTruthy()expect(screen.queryByText('Hello')).toBeFalsy()})})

一見問題はないのですが、このテストの場合 axios をそのまま mock しているので axios のモジュール自体にバグが入り機能しなくなった場合や、BREAKING CHANGE で axios の戻り値が変化した場合(例えばdataでのラップがなくなるなど)でもテストは通過してしまいます。

mswを使ってネットワークレベルでモックしたコード

続いて msw を使った例です。setupServer/loginへの POST リクエストに対してインターセプトの定義を行っています。内部でリクエストの username と password の比較を行い正常系と異常系のレスポンスを分けています。
テストコードはとてもシンプルです。ただフォームに値を入れて送信しているだけです。

Login.test.ts
import{ rest}from"msw"import{ render, screen, fireEvent}from'@testing-library/vue'import{ setupServer}from"msw/node";import Loginfrom"../../components/Login.vue"constVALID_USER={  username:'validUsername',  password:'validPassword'}const mockServer=setupServer(  rest.post<Record<string,any>>('/login',(req, res, ctx)=>{const{ username, password}= req.bodyif(username!==VALID_USER.username&& password!==VALID_USER.password){returnres(        ctx.status(403),        ctx.json({          error:'error: invalid username or password'}))}returnres(      ctx.status(200),      ctx.json({        name:'validUsername',}))}))describe('Login',()=>{beforeAll(()=> mockServer.listen())afterEach(()=> mockServer.resetHandlers())afterAll(()=> mockServer.close())test('ログインが成功した場合にユーザー名を表示する',async()=>{render(Login)expect(screen.queryByText('Hello, validUsername')).toBeFalsy()await fireEvent.update(screen.getByLabelText(/username/i),VALID_USER.username)await fireEvent.update(screen.getByLabelText(/password/i),VALID_USER.password)await fireEvent.click(screen.getByText('submit'))expect(await screen.findByText('Hello, validUsername')).toBeTruthy()expect(screen.queryByTestId('error')).toBeFalsy()})test('ログインが失敗した場合にエラーを表示する',async()=>{render(Login)expect(screen.queryByText('Hello, validUsername')).toBeFalsy()await fireEvent.update(screen.getByLabelText(/username/i),'invalid user')await fireEvent.update(screen.getByLabelText(/password/i),'invalid password')await fireEvent.click(screen.getByText('submit'))expect(await screen.findByTestId('error')).toBeTruthy()expect(screen.queryByText('Hello')).toBeFalsy()})})

これなら、jest.mockを使った場合と異なり、axios のモジュール自体のバグや、BREAKING CHANGE で戻り値が変わった場合にはテストが失敗します。より安全なテストとなります。

Nock との比較

msw のようなネットワークレベルでのインターセプトライブラリとしてはnockも有名です。

https://github.com/nock/nock

msw のドキュメントに nock との比較があったので記載します。

基準NockMock Service Worker
サポートする APIRESTREST / GraphQL
環境NodeNode / Browser
実装http/https/XMLHttpRequest モジュールにモンキーパッチを当てることhttp/https/XMLHttpRequest モジュールにモンキーパッチを当てる
インテグレーション既存のコードに変更を加える必要はない。axios などのリクエスト発行ライブラリに対応したアダプタが必要。既存のコードに変更を加える必要はない。アダプタも不要。
定義メソッドチェインによるモックの定義関数定義によるモックの定義

Nock と比較すると GraphQL に対応している点、Browser でも使える点、アダプタが不要な点が良さそうです。
他のライブラリとの比較もこちらにあります。

https://mswjs.io/docs/comparison

終わりに

以上、「Mock Service Worker で jest.mock を使わず非同期リクエストのテストを書く」でした。jest.mockを使う場合と比べ少し記述量は増えますが、よりテストでの安全性を担保できるので良さそうです。また、パスの間違えなどで地味にモックされず困ることもなくなります。今後、業務でも利用していきたいです。

ryo_kawamata

Engineer@lapras.inc / TypeScript / Vue.js / Firebase / GraphQL / 元消防士 / 懸垂

バッジを贈って著者を応援しよう

バッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。

ZUKU(お役に立てたら👍イイねオネガイシマース)ZUKU(お役に立てたら👍イイねオネガイシマース)

はじめまして。
突然のコメント失礼致します。
大変わかりやすい記事で参考にさせていただいております。
確認させていただきたいことがありましたのでコメントさせていただきました。
>ネットワークレベルで API リクエスト
・”ネットワークレベルで”とはどういうことでしょうか?
 
>mock のレイヤーが低レイヤーになるほどテストがより安全になる
・レイヤーが低いとはどういうことでしょうか?
・そしてなぜレイヤーが低いとテストが安全になるのでしょうか
 
知識が浅く用語の質問にあり大変恐縮です。
もし宜しければご回答お願い致します。


[8]ページ先頭

©2009-2025 Movatter.jp