5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

セーフィー株式会社Advent Calendar 2022

Day 21

Mock Service Workerをフロントエンドテストに導入してみた

Last updated at Posted at 2022-12-22

はじめに

セーフィー株式会社でフロントエンドエンジニアをやっている @ungi_podo です。
この記事はSafie Engineers' Blog! Advent Calendar 21日目の記事です。

従来だとapiを利用したテストを書くにはjestのモックを利用してaxiosなどのモジュールをモック化していたと思います。
最近フロントエンドテストではmswを使用してapiを利用したテストを書くことが注目されているみたいなので導入してみました!

Mock Service Workerとは

Mock Service Worker(以下MSW)とはService Workerを通じてAPIリクエストをインターセプトし、mockのレスポンスを返すことができるライブラリです。

公式にわかりやすいフロー図があったので紹介します。
image.png

他に開発用mockだとJson Serverが有名ですが、
MSWではservice workerを利用し、リクエストを返すため別プロセスでローカルサーバーを立てる必要なく利用ができるためかなり便利です。

  • モックを高レイヤーで実装してしまうと、低レイヤーに変更が入った際にモックが正確ではないのにテストが通る状態が発生する。またモックの修正範囲が広くなる。
  • テストファイル内にモックなどのセットアップのためのコードをなくすことが出来るので目的のテストコードの実装のみに集中することが出来る。
  • サーバーを別途立てずに利用が可能でテスト、開発中で利用できたり、storybookでも利用が可能。
    などの利点があると思ったので導入しようと考えました。

詳しい内容はMSWの公式をご覧ください。

テスト対象のファイル

ただのデータを取得して返している関数になります。
こちらを今回テストしていきたいと思います。

src/repository/devices.ts
import axios from 'axios'

type Parmas = {
  name: string
  deviceId: number
}

const getDevices = async (params: Parmas) => {
  const response = await axios.get('/users', params)
  return response
}

実装方法

apiモックの作成

src/mocks配下にhandlers.tsを作成し下記のように実装します。

公式ではhandlersにモックapiの定義も書いていますが、
ファイルが大きくなりそうなのでhandlersと同じ階層にmock.tsというモックのapiの定義を書いて対応しようと思います。

src/mocks/devices/handlers.ts
import { rest } from 'msw'
import devices from '@/mocks/devices/mock'

export const handlers = [rest.get('/users', users.getUsers)]
src/mocks/users/mock.ts
import { ResponseResolver, MockedRequest, restContext } from 'msw'

const getDevices: ResponseResolver<MockedRequest, typeof restContext> = (req, res, ctx) => {
  return res(
    ctx.status(200),
    ctx.json([
      {
        deviceId: 1,
        name: 'dummy1',
      },
      {
        deviceId: 2,
        name: 'dummy2',
      }
    ])
  )
}

export default { getDevices }

モジュールを分けてしまうと型を別でつけないといけなくなるので、つけています。

モックapiをテストで利用できるようにサーバーを立てる

yarn
> touch src/mocks/server.ts

server.tsファイルでは定義したリクエストハンドラーを使用してサーバーインスタンスを作成します。

src/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

// これによってモックサーバーが構成されます。
export const server = setupServer(...handlers)

mswを利用したjestのテスト実装

src/test/devices.spec.ts
import { server } from '~/src/mocks/server'
import { getDevices } from '~/src/repository/devices.ts'

describe('devices', () => {
  beforeAll(() => server.listen()) // サーバーを起動する
  beforeEach(() => server.resetHandlers()) // リクエストハンドラをリセット
  afterAll(() => server.close()) // サーバーを停止する
  
  describe('getDevices', () => {
    test('データ取得を行い、返ってきたデータを返していること', async () => {
      const response = await getDevices()
      
      expect(response).toStrictEqual([
        {
          deviceId: 1,
          name: 'dummy1',
        },
        {
          deviceId: 2,
          name: 'dummy2',
        }
      ])
    })
  })
}

先ほど作成したモックサーバーをインポートし、beforeAllでテストを始める前にサーバーを起動させます。
そしてafterAllでテストが終わった後サーバーを停止させています。

テストで引数も検査したい

テストする中で渡している引数が問題ないかを確認したい場合があると思います。
ただ、msw内で引数を検査するのが面倒くさいとのことだったので、jest.fnを利用して引数を確認しようと思います。

mockを定義したファイルにjest.fnを仕込みます。

src/mocks/users/mock.ts
import { ResponseResolver, MockedRequest, restContext } from 'msw'

+ type Parmas = {
+   name?: string
+   deviceId?: number
+ }

+ export const spyGetUserParams = jest.fn<void,[Parmas]>()

const getDevices: ResponseResolver<MockedRequest, typeof restContext> = (req, res, ctx) => {
+  spyGetUserParams(req.body)

  return res(
    ctx.status(200),
    ctx.json([
      {
        deviceId: 1,
        name: 'dummy1',
      },
      {
        deviceId: 2,
        name: 'dummy2',
      }
    ])
  )
}

export default { getDevices }

そしてテストファイルで先ほどのjest.fnを利用して引数を検査します。

src/test/devices.spec.ts
import { server } from '~/src/mocks/server'
import { getDevices } from '~/src/repository/devices.ts'
import { spyGetUserParams } from '~/src/mocks/users/mock.ts'

describe('devices', () => {
  beforeAll(() => server.listen()) 
  beforeEach(() => {
    server.resetHandlers()
+   spyGetUserParams.mockClear()
  )
  afterAll(() => server.close())

  describe('getDevices', () => {
    // ...
+   test('getDevicesの引数の検査', async () => {
+     const params = {
+       name: 'hoge'
+     }
+     const response = await getDevices(params)
+
+     expect(spyGetUserParams.mock.calls[0][0].name).toBe('hoge')
+   })
  })
}

モックを定義したファイルから仕込んでおいたjest.fnを取得して引数の検査を行なっています。
今のところ力技ですが、このような感じで検査するしかなさそうですね。。
(他に良いやり方があればコメントいただきたいです。)

終わりに

mswを導入して思ったんですが、本当に導入が簡単でした。
別でサーバーを立てる必要もなく、インストールするだけで利用ができるのでやはり便利ですね。
今はフロントエンドテストでしか利用できていませんが、これから開発用でモックapiとして利用したりstorybookでも利用していきたいと思います。
そしたら別でまた記事を書こうと思います〜

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?