Mock Service Workerを使ってみた

:::note warn
この記事は、[Supershipグループ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/supership)の19日目の記事になります。
:::

# はじめに

Mock Service Workerの紹介の記事になります。

# Mock Service Workerとは

https://v1.mswjs.io/

モックのライブラリで、よくあるモックのライブラリより便利だった点は以下です

* アプリケーション側の通信するコードを書き換えずにモックができる 
  * アプリケーション側の通信する処理のコードで例えば環境変数だったりで、テスト時には通信しないというような分岐のコードなどの書き換えが必要がなかった 

* モックサーバーを別に起動する必要がない
  * 開発時にでモック用のサーバーが必要になった場合、モック用のサーバー作って別に立ち上げるという流れが多いがブラウザやNode.jsからそのまま動かせる

## テストコードで使う

```json:package.json
{
  "scripts": {
    "test": "vitest"
  },
  "dependencies": {
    "msw": "2.0.11",
    "vitest": "1.0.4"
  }
}
```

## vitestのspyOnでモックした例

```js
import { afterEach, vi, describe, expect, test } from 'vitest'

const Api = {
  fetchPost: async () => {
    const response = await fetch('https://text.example.com/posts')
    return await response.json()
  }
}

describe('post', () => {
  const response = {
    posts: [{ id: '1', name: 'name1' }, { id: '2', name: 'name1' }]
  }

  afterEach(() => {
    vi.resetAllMocks()
  })

  test('get posts', async () => {
    vi.spyOn(Api, 'fetchPost').mockResolvedValue(response)
    expect(await Api.fetchPost()).toEqual(response)
  })
})
```

* 外部のAPIに対して通信する処理があるケースでは、テスト実行する毎に実際に通信するわけにもいかないのでモックすることが多いですが、その場合の課題点としてモックした部分に　`fetchPost` に独自ロジックが入ってた場合にその部分のテストができない点があります。

* `vi.spyOn(global, 'fetch')`で`fetch` 自体もモックしてしまう手もありますが影響範囲が大きくなってしまう課題点があります。


## Mock Service Workerを使った例

```js
import { beforeAll, afterEach, afterAll, describe, expect, test } from 'vitest'
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'

const Api = {
  fetchPost: async () => {
    const response = await fetch('https://text.example.com/posts')
    return await response.json()
  }
}

describe('posts', () => {
  const response = {
    posts: [{ id: '1', name: 'name1' }, { id: '2', name: 'name1' }]
  }

  const server = setupServer(
    http.get('https://text.example.com/posts', () => {
      return HttpResponse.json(response)
    })
  )

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

  test('get posts', async () => {
    expect(await Api.fetchPost()).toEqual(response)
  })
})
```

Mock Service Workerを使うと、元々の通信する処理に `fetchPost` に対して手を加えずにモックできるので、`fetchPost`の中にロジックが書かれていてもそこもそのままテストできます。

# 開発時にモックとして使う

```json:package.json
{
  "scripts": {
    "start": "vite"
  },
  "dependencies": {
    "msw": "2.0.11",
    "vite": "5.0.10"
  }
}
```

```js:mock.js
import { http, HttpResponse } from 'msw'
import { setupWorker } from 'msw/browser';

const response = {
  posts: [{ id: '1', name: 'name1' }, { id: '2', name: 'name1' }]
}

export const worker = setupWorker(
  http.get('https://text.example.com/posts', () => {
    return HttpResponse.json(response)
  })
)
```

```js:index.js
const prepare = async () => {
  if (import.meta.env.VITE_MOCK_API === 'true') {
    const { worker } = await import('./mock')
    worker.start()
  }
}

const main = async () => {
  const response = await fetch('https://text.example.com/posts')
  await response.json()
}

window.onload = () => {
  prepare().then(async () => await main())
}
```

### 開発時にモックとして使う場合

* 環境変数でモックの有効、無効を切り替えるようにしておく
  * プロダクション環境でもモックのままになってしまうので

* モックのコードはDynamic Importするようにしておく
  * Dynamic ImportにしておくとJavaScriptをビルドした時のバンドルが分かれるので、プロダクション環境のデプロイ時にはモックのバンドルはアップロードする必要がないため

```sh
yarn run msw init ./public
VITE_MOCK_API=true yarn run start
VITE v5.0.10  ready in 495 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help
```

![スクリーンショット 2023-12-18 23.35.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21172/e5892643-24d4-4285-ab11-2f850e6e9aa4.png)

このようなかたちで実際のurlのサイトにはアクセスせず、アクセスしたようにレスポンスを返せます。

# 最後に

色々と活用できそうなので、興味ある方は試してみると良いかと思います。

* 送ってきたパラメータによって返すモックのレスポンスを変える
* apiとfrontendの担当者が別の場合に先にモックのapiを作ってしまってapiの実装完了待ちせずに画面の実装を並行して開発を進める

:::note
**最後に宣伝です。**
Supershipではプロダクト開発やサービス開発に関わる人を絶賛募集しております。
ご興味がある方は以下リンクよりご確認ください。
[Supership 採用サイト](https://supership.jp/recruit/)
是非ともよろしくお願いします。
:::