MSW (Mock Server Worker)を使ったAPI周りのテストメモ

# 概要
入社前の研修として作成していたTodoアプリのテスト実装時に、API周りのテスト実装で詰まってしまったので、備忘録としてこの記事を書いています。

# API周りのテストについて
API周りのテストを行う方法としては、以下のようなものがあります。
### 各テストごとにfetchそのものをモックする
自分が最初に実践したのはこの方法です。
以下の記事を参考に実装を行いました。

[非同期リクエストを扱うコンポーネントのテスト：fetch そのものをモック、実装編](https://zenn.dev/tkdn/books/react-testing-patterns/viewer/testing-with-fetchmock#0.-%E3%83%A2%E3%83%83%E3%82%AF%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E4%B8%8B%E6%BA%96%E5%82%99)

モックを作成しておき...

```typescript
const loginMock = () =>
  new Promise((resolve) => {
    resolve({
      ok: true,
      status: 200,
      json: async () => ({ token: mockToken }),
    });
  });

```

テストごとに応じて、fetchにモックを適用させる。
```typescript
describe('ログインテスト', () => {
  global.fetch = jest.fn().mockImplementation(loginMock);
  //...
}
```

この方法は、実装が非常に簡単でした。
しかし...
- 各テストごとにいちいちモックを適用しなくてはならない
- 各テストごとに１つしかAPI処理をモック出来ないため、柔軟性が低い （これがキツイ...）

こういった点から、この方法はボツに。


### ApolloやMSWを使ってモックサーバーを作成する
モックサーバーを作成する方法はいくつかありましたが、
今回は手軽さを考えて、MSWを使用しました。
(GraphQLについての知識もなかったので…👼)
実装方法について、次項から記述していきます。

# モックサーバーを作成していく
いい感じに例がありましたので、こちらのリポジトリを参考に実装していきます。
[mswjs examples (github)](https://github.com/mswjs/examples/tree/master/examples/with-jest)
この記事では、ユーザー登録・ログインについてモックを作成していきます。

### MSWを導入
- npm
```
npm install -D msw
```
- yarn
```
yarn add msw --dev
```

### ファイル構成
```
src
 | --- mocks
 |      | --- server.ts
 |      | --- handlers
 |               | --- handlers.ts
 |               | --- auth.ts
 |
 | --- __tests__ 
 |
 | --- jest.config.js
 | --- jest.setup.js
 .
 .
 .
```

#### jest.setup.js
```javascript
import 'whatwg-fetch'; //テスト環境ではfetchがないため、別ライブラリから用意

import { server } from './mocks/server';

beforeAll(() => {
  server.listen(); //モックサーバーの起動
});

afterEach(() => {
  server.resetHandlers();
});

afterAll(() => {
  server.close(); //モックサーバーの停止
});
```

#### jest.config.js
```javascript
module.exports = {
//テスト実行時にjest.setup.jsの処理を実行する
  setupFilesAfterEnv: ['./jest.setup.js'],
};
```

#### server.ts
```typescript
import { setupServer, SetupServerApi } from 'msw/node';
import { handlers } from './handlers/handlers';

export * from 'msw';

export const server: SetupServerApi = setupServer(...handlers);
```

#### handlers.ts （各モックのハンドラーをここでまとめてexport）
```typescript
import { handlers as authHandlers } from './auth';

export const handlers = [...authHandlers];
```

#### auth.ts
```typescript
import { rest } from 'msw';

interface AuthRequestBody {
  email: string;
  password: string;
}

export let users = [
  {
    id: '1',
    email: 'test@test.com',
    password: 'testtest',
  },
];

export const mockToken = 'mocktoken-test';

const loginHandler = rest.post<AuthRequestBody>(
  `${process.env.NEXT_PUBLIC_API_URL}/users/login`,
  (req, res, ctx) => {
    const { email, password } = req.body;

    if (email === '' || password === '') {
      return res(ctx.status(500));
    }

    const match = users.filter((user) => {
      return user.email === email && user.password === password;
    });

    if (false) {
      return res(ctx.status(401));
    } else {
      return res(
        ctx.status(200),
        ctx.json({
          token: mockToken,
        })
      );
    }
  }
);

const registerHandler = rest.post<AuthRequestBody>(
  `${process.env.NEXT_PUBLIC_API_URL}/users`,
  (req, res, ctx) => {
    const { email, password } = req.body;

    if (email === '' || password === '') {
      return res(ctx.status(500));
    }

    const match = users.filter((user) => {
      return user.email === email;
    });

    if (match.length !== 0) {
      return res(ctx.status(409));
    } else {
      return res(
        ctx.status(200),
        ctx.json({
          email: email,
        })
      );
    }
  }
);

export const handlers = [loginHandler, registerHandler];
```

このようにコードを書き、テストを実行するとモックサーバーが起動します。
後は環境に合わせてテストコードを書いていきましょう。

例えば、自分の場合は以下のようなコードを書きました。
```typescript
import React from 'react';
import Login from '../pages/login';
import { render, cleanup } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

jest.mock('next/router', () => ({
  useRouter() {
    return {
      push: jest.fn(String),
    };
  },
}));

afterEach(cleanup);


describe('ログインテスト', () => {

  it('inputの入力テスト', () => {
    const { getByPlaceholderText } = render(<Login />);
    const emailInput = getByPlaceholderText('email') as HTMLInputElement;
    const passwordInput = getByPlaceholderText('password') as HTMLInputElement;
    userEvent.type(emailInput, 'test@test.com');
    userEvent.type(passwordInput, 'testtest');
    expect(emailInput.value).toBe('test@test.com');
    expect(passwordInput.value).toBe('testtest');
  });

  it('ログイン成功時のテスト', async () => {
    const { getByPlaceholderText, getByText, findByText } = render(<Login />);
    const emailInput = getByPlaceholderText('email') as HTMLInputElement;
    const passwordInput = getByPlaceholderText('password') as HTMLInputElement;
    const loginButton = getByText('Login');
    userEvent.type(emailInput, 'test@test.com');
    userEvent.type(passwordInput, 'testtest');
    userEvent.click(loginButton);
    expect(findByText('All')).toBeTruthy();
  });
});
```

# MSWを利用して思ったこと
- 思った以上に実装が手軽に行えた。
- 学習コストもあまり高くない。
- Responseのステータスも柔軟に設定可能。

# おわりに
MSWの利用方法について備忘録を書きました。
簡単に扱えたので、今後もお世話になりそうです。
今後の状況次第だけど、Apolloも試してみたいな...

# 参考にさせて頂いたサイト
- [非同期リクエストを扱うコンポーネントのテスト：fetch そのものをモック、実装編](https://zenn.dev/tkdn/books/react-testing-patterns/viewer/testing-with-fetchmock#0.-%E3%83%A2%E3%83%83%E3%82%AF%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E4%B8%8B%E6%BA%96%E5%82%99)
- [mswjs examples (github)](https://github.com/mswjs/examples/tree/master/examples/with-jest)