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

# はじめに
セーフィー株式会社でフロントエンドエンジニアをやっている @ungi_podo です。
この記事は[Safie Engineers' Blog! Advent Calendar](https://qiita.com/advent-calendar/2022/safie) 21日目の記事です。

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

# Mock Service Workerとは
[Mock Service Worker](https://mswjs.io/)（以下MSW）とはService Workerを通じてAPIリクエストをインターセプトし、mockのレスポンスを返すことができるライブラリです。

公式にわかりやすいフロー図があったので紹介します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1268363/63f4dc46-f2d0-8610-1366-dee02a59074f.png)

他に開発用mockだと[Json Server](https://github.com/typicode/json-server)が有名ですが、
MSWではservice workerを利用し、リクエストを返すため別プロセスでローカルサーバーを立てる必要なく利用ができるためかなり便利です。


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


詳しい内容は[MSWの公式](https://mswjs.io/docs/)をご覧ください。

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

```typescript: 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の定義を書いて対応しようと思います。

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

export const handlers = [rest.get('/users', users.getUsers)]
```

```typescript: 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をテストで利用できるようにサーバーを立てる

```bash: yarn
> touch src/mocks/server.ts
```

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

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

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

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

```typescript: 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を仕込みます。
```diff_typescript: 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を利用して引数を検査します。
```diff_typescript: 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でも利用していきたいと思います。
そしたら別でまた記事を書こうと思います〜