mswのバージョン2.0.0で行われた破壊的変更を確認する

## msw
[msw](https://mswjs.io)はブラウザー、Node.jsで用いられるAPIモックライブラリです。特定のリクエストを傍受し定めた方法で処理できます。
Agnostic、Seamless、Reusableの3つを特徴として掲げています。
- Agnostic
    - 環境・フレームワーク・ツールに依存せず利用可能
- Seamless
    - ネットワークレベルで傍受するのでアプリケーションレベルの整合性に干渉しない
- Reusable
    - 環境・フレームワーク・ツールそれぞれでAPIモックを共有可能

そんなmswですが、2023年10月23日に[バージョン2.0.0](https://github.com/mswjs/msw/releases/tag/v2.0.0)がリリースされました。バージョン2.0.0はそれ以前のバージョンと比べて記法が大きく変わります。この記事では2.0.0で変更された記法について紹介します(`graphql`については詳しくないので`http`リクエストについて注目して紹介します)。
Node.jsの18.0.0、TypeScriptの4.7以降の環境でしか動作しないことに注意してください。

### `setupWorker`
これまでブラウザーでmswを有効にするには`msw`から`setupWorker`を呼び出して以下のように記述していました。
```ts
import { setupWorker } from 'msw';

const worker = setupWorker(...handlers);
worker.start();
```
基本的な利用方法は変わりませんが、バージョン2.0.0では`msw/browser`からインポートする必要があります。
```ts
import { setupWorker } from 'msw/browser';

const worker = setupWorker(...handlers);
worker.start();
```

### リクエストハンドラの定義
`setupWorker`に定義した`handlers`は以下のように`rest`を用いたリクエストハンドラーを配列で定義していました。
```ts
import { rest, setupWorker } from 'msw';

setupWorker([
  rest.get('/events', mockEvents),
]);
```
これからは`http`を用いて定義します。

```ts
import { graphql, http } from 'msw';

http.get('/events', mockEvents);
```
HTTPメソッドを取り出す部分や第1引数にパスを渡す部分は変わっていませんが、第2引数に渡すレスポンスリゾルバの記述方法が変更されました。さらに、第3引数としてハンドラオプションを渡せるようになりました。

### レスポンスリゾルバ
先ほど記述した`mockEvents`のようなレスポンスリゾルバを定義する方法も変更されました。
これまでは引数で`req`と`res`、`ctx`の3つを受け取ってそれを元に処理を組み立てていました。
```ts
import type { ResponseResolver, MockedRequest, restContext } from 'msw';

const mockEvents: ResponseResolver<RestRequest, RestContext> = (
  req,
  res,
  ctx,
) => {
  return res(ctx.status(200), ctx.json({}));
};
```
今後はそれらの引数を用いて処理を定義する方式が廃止されます。上記の例を2.0.0形式で書くと新たなAPIである`HttpResponse`を用いて以下のように記述します。
```ts
import { type ResponseResolver, HttpResponse } from 'msw';

const mockEvents: ResponseResolver = () => {
  return HttpResponse.json({});
};
```
以下のようにFetch APIの`Response`を用いることでも記述できます。
```ts
import { type ResponseResolver, HttpResponse } from 'msw';

const mockEvents: ResponseResolver = () => {
  return new Response(
    JSON.stringify({}),
    {
      headers: {
        'Content-Type': 'application/json',
      },
    },
  );
};
```
`HttpResponse`は`Response`に比べてにレスポンスクッキーのモックのような特定な機能を可能にします。公式では一貫性のために特別な理由がない限り`HttpResponse`を使用することを推奨しています。

レスポンスリゾルバでは既存の3つの引数が廃止された代わりに、新たに1つのオブジェクト形式の引数を取れるようになりました。
```ts
import { type ResponseResolver, HttpResponse } from 'msw';

const mockEvents: ResponseResolver = ({
  request,
  params,
  cookies,
}) => {
  return HttpResponse.json({});
};
```
オブジェクトは`request`と`params`と`coolies`の3つの値を持ちます。`request`はFetch APIの`Request`、`params`は`Record<string, string[] | string>`、`cookies`は`Record<string, string>`の型を持ちます。これらを用いてより直感的に処理を記述できるようになりました。
```ts
import { type ResponseResolver, HttpResponse } from 'msw';
 
const mockEvents: ResponseResolver = ({
  request,
  params,
  cookies,
}) => {
  const url = URL(request.url)
  const { id } = params;
  const { token } = cookies;
})
```
#### passthrough
レスポンスリゾルバで処理後にそのままリクエストを実行させる場合について考えます。これまでは第1引数の`req`を用いて以下のように記述していました。
```ts
import type { ResponseResolver, MockedRequest, restContext } from 'msw';

const mockEvents: ResponseResolver<RestRequest, RestContext> = (
  req,
) => {
  return req.passthrough();
};
```
2.0.0からは`req`を用いることはできないので、`msw`から`passtrough`を新たにインポートして記述します。
```ts
import { passthrough, type ResponseResolver } from 'msw';

const mockEvents: ResponseResolver = () => {
  return passthrough();
};
```

#### deplay
処理の遅延もこれまでは、第3引数の`ctx`を用いて以下のように行なっていました。
```ts
import type { ResponseResolver, MockedRequest, restContext } from 'msw';

const mockEvents: ResponseResolver<RestRequest, RestContext> = (
  req,
  res,
  ctx,
) => {
  return res(
    ctx.delay(100),
    ctx.status(200),
    ctx.json({}),
  );
};
```
今後は`delay`を`msw`からインポートして行います。
```ts
import { delay, type ResponseResolver, HttpResponse } from 'msw';

const mockEvents: ResponseResolver = () => {
  delay(100);
  return HttpResponse.json({});
};
```
#### bypass
レスポンスリゾルバ内でmswのAPIモックに関わらないネットワーク上でHTTPリクエストを行うことを考えます。
これまでは`ctx`が持つ`fetch`を元に通信していました。
```ts
import type { ResponseResolver, MockedRequest, restContext } from 'msw';

const mockEvents: ResponseResolver<RestRequest, RestContext> = (
  req,
  res,
  ctx,
) => {
  const response = await ctx.fetch(req);
  const resopnseJson = await response.json();
  return res(
    ctx.status(200),
    ctx.json({
      ...responseJson,
    }),
  );
};
```
今後は`msw`から`bypass`をインポートして以下のように書きます。
```ts
import { bypass, type ResponseResolver, HttpResponse } from 'msw';

const mockEvents: ResponseResolver = ({
  request,
}) => {
  const response = await fetch(bypass(request));
  const resopnseJson = await response.json();
  return HttpResponse.json({
    ...responseJson,
  });
};
```

### once
エラー処理の検証のために特定のレスポンスリゾルバの処理を1回だけ実行したいケースがあります。これまではレスポンスリゾルバ内で第2引数の`res`を用いて以下のように書く必要がありました。
```ts
import { rest, setupWorker } from 'msw';
import type { ResponseResolver, MockedRequest, restContext } from 'msw';

const mockEvents: ResponseResolver<RestRequest, RestContext> = (
  req,
  res,
  ctx,
) => {
  return res.once(
    ctx.status(200),
    ctx.json({
      ...responseJson,
    }),
  );
};
setupWorker([
  rest.get('/events', mockEvents),
]);
```
今後はレスポンスリゾルバではなく、リクエストハンドラの第3引数で指定します(ハンドラのオプションとして渡せるのは現在`once`だけです)。
```ts
import { type ResponseResolver, HttpResponse } from 'msw';

import { setupWorker } from 'msw/browser';

const mockEvents: ResponseResolver = () => {
  return HttpResponse.json({});
};

const worker = setupWorker([
  http.get('/events', mockEvents, {
    once: true,
  }),
]);
```
これまではハンドラのオプションについてもリゾルバに定義していて再利用が難しかったり、役割におけるコードの分割がうまくできていなかった箇所が解決したのでとても使い心地が良くなりました。

## さいごに
`msw`のバージョン2.0.0の変更を紹介しました。破壊的変更が多く、バージョンアップには苦労しそうですが、記述方法などは直感的でわかりやすく実現したいことをそのまま表現しやすくなったように感じました。