MSW (Mock Service Worker) × Storybook を利用したVRT について

[Ateam LifeDesign Advent Calendar 2023](https://qiita.com/advent-calendar/2023/ateam-life-design)、投稿します。


## はじめに
私は現在、暮らしの「まよい」を解決する情報メディアの[「イーデス」](https://www.a-tm.co.jp/top/)というサービスの開発を担当しています。

https://www.a-tm.co.jp/top/

このサービスは、クレジットカードやカードローン、住宅ローンなど「生活のお金」に関する情報をはじめ、証券投資やふるさと納税など「投資・節税」に関する情報等、様々な情報を発信し、一人ひとりの「まよい」を解決し、
お客様が「よかった」と思える体験をするためのコンテンツを様々用意している 総合メディア です。

今回の記事は、この「イーデス」の 特定のUIコンポーネントを VRT（Visual Regression Test） する際に利用している、MSW（Mock Service Worker） について紹介します。

:::note warn
本記事で書かないこと
・Storybook / VRT / GraphQL の導入理由や経緯など
:::


## サービスのざっくり仕様
- 「イーデス」は Next.js + React + TypeScript で構成されています。
- データのクエリ言語 は GraphQL を使用しています。
- VRT は storybook(v7) + storycap + reg-suit で構成されています。


## 課題を一言で
- **「ショートコード の UIコンポーネント の VRT ができない！」**
→ Web API に依存したStory が表示できない。（バックエンドのデータが必要）
- 課題の詳細 は下記に記載

### 課題の詳細
- 「イーデス」の、通常のUIコンポーネントに対するStoryファイル のほとんどは、それぞれで使用されている props を args として渡したり 簡単なMockデータ を渡したりして Storyを表示させ、screenshot するような VRT が実現できています。
- ここで、コンテンツで頻繁に利用 かつ Web APIに依存した主要なUIコンポーネント は、記事作成するライター が管理画面で利用しやすいよう、ショートコード化 されております。
- しかし、ショートコード化されたUIコンポーネントのStory は、バックエンドのデータが無いと表示できないので、本来であれば 本番環境のAPIサーバー へリクエストを送り、データを取得する必要 が出てきます。
- そして、これを何も考えずにそのまま行なってしまうと、セキュリティリスクの問題 や 実際のAPIに依存した開発 になってしまう懸念などがあるため、できれば避けたいです。
- そこで、`本番のAPIサーバーへリクエストを送ることなく、バックエンドのデータを利用できるようにしたい`という際に便利なのが `MSW （Mock Service Worker）` です。


## MSW (Mock Service Worker) とは
- MSW は、ネットワークレベル で APIのリクエストを`インターセプト`して mockのレスポンスを返してくれる、APIモックライブラリ です。
  - `インターセプト`： ググると直訳で「横取り・傍受」などという意味ですが、
  ここでは「通信やデータの流れを検知し、その内容を読み取る」という意味解釈が正しいですね。

https://mswjs.io/

- サッと仕組みの理解は、こちらの公式ビデオを

https://youtu.be/HcQCqboatZk


### MSW を利用するメリット
#### 1. 実際のAPI を叩かないので サーバー負荷をかけない。 そして APIに依存せず開発が可能
- MSWは、実際のサーバーへリクエストを行う代わりに、リクエストをインターセプトしてモックデータを返すことができます。
- ネットワークレベル で リクエストをインターセプトできる という点がキモで、これにより、サーバー負荷もかけないし、リソースの浪費 や 実際のサーバー(API)への依存 などの問題 も解消できます。
- ネットワークレベルで ということなので、DevTools の Networkタブ で リクエストとレスポンス を簡単に確認できる点もメリットですね。

#### 2. GraphQL や REST のAPI に対応することができる
- 今回は GraphQL で利用したいので言わずもがな。

#### 3. Mockサーバ用のプロセス を別で立てる必要がない
- MSW は Service Worker を使用してリクエストを返すので、別プロセスでローカルサーバーを立てることなく簡単に利用することができます。
  - サーバーを立てる必要がある JSON Server とよく比較されるそうですね。
  - Service Worker って何？ という方はこちら↓

https://developer.mozilla.org/ja/docs/Web/API/Service_Worker_API




## MSW の使い方
具体的な使い方について、全体の流れは以下です。

**⓪ (GraphQL環境 を用意)**
**① 必要なpackageの追加 と storybook設定ファイル の調整**
**② Service Worker のコードを生成**
**③ モックAPI の定義 を追加**

------------------------------------------------------------------------
### ⓪ (GraphQL環境 を用意)
- 今回は、GraphQLを利用している環境でのユースケースなので、その環境を再現します。

#### 以下コマンドで、 `graphql` と `@apollo/client` を追加
```zsh
$ npm i graphql @apollo/client
```
- （※当方、パッケージマネージャーは yarn の1系 を使用しているのですが、Storybook v7 をインストールする際に 特有の依存関係エラーが発生したため、今回は npm を使用しています。）
  - 参照Issue：[[Bug]: string-width dependency stops storybook from executing](https://github.com/storybookjs/storybook/issues/22431)

#### 例として、以下のような UserListコンポーネント と そのStoryファイル を作成
- UserList コンポーネント を作成します。
```tsx
// src/components/UserListContainer.tsx

import { gql, useQuery } from "@apollo/client";
import { UserList } from "./UserList";

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      username
      email
    }
  }
`;

const useGetUsers = () => {
  const { data, loading, error } = useQuery(GET_USERS);
  const users = data ? data.users : [];
  return { users, loading, error };
};

export const UserListContainer = () => {
  const { users, loading, error } = useGetUsers();

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error! {error.message}</p>;

  return <UserList users={users} />;
};
```

```tsx
// src/components/UserList.tsx

import "./UserList.css";

type Props = {
  users: {
    id: number;
    name: string;
    email: string;
  }[];
};

export const UserList = ({ users }: Props) => {
  return (
    <div className="user-list">
      <h2>MSW User List</h2>
      <ul>
        {users.map((user) => (
          <li key={user.id} className="user-item">
            <div className="user-info">
              <h3>{user.name}</h3>
              <p>{user.email}</p>
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
};
```

```css
/* src/components/UserList.css */

.user-list {
  width: 60%;
  margin: 0 auto;
}
.user-list h2 {
  text-align: center;
  margin-bottom: 30px;
}
.user-list ul {
  list-style: none;
  padding: 0;
}
.user-list li {
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.user-item {
  display: flex;
  align-items: center;
  padding: 20px;
  border-bottom: 1px solid #ccc;
}
.user-info h3 {
  margin-bottom: 5px;
}
```

- 先出しですが、モックファイルも作成しておきます。


```tsx
// src/mocks/users-data.ts

export const mockUsers = [
  {
    id: 1,
    name: "MSW John",
    email: "John@example.com",
  },
  {
    id: 2,
    name: "MSW Jane",
    email: "Jane@example.com",
  },
  {
    id: 3,
    name: "MSW Bob",
    email: "Bob@example.com",
  },
];
```

```tsx
// src/mocks/MockApolloProvider.tsx

import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client";

type MockApolloProviderProps = {
  children: React.ReactElement;
};

const client = new ApolloClient({
  uri: "fake.gql.server",
  cache: new InMemoryCache(),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: "no-cache",
      errorPolicy: "all",
    },
    query: {
      fetchPolicy: "no-cache",
      errorPolicy: "all",
    },
  },
});

export const MockApolloProvider = ({ children }: MockApolloProviderProps) => {
  return <ApolloProvider client={client}>{children}</ApolloProvider>;
};
```

- UserList のStoryファイル を作成します。
  - （CSF3 - storybook v7 の記述方法です。）
```tsx
// src/components/UserList.stories.tsx

import type { Meta, StoryObj } from "@storybook/react";
import { MockApolloProvider } from "../../mocks/MockApolloProvider";
import { handlers } from "../../mocks/handlers";
import { UserListContainer } from "./UserListContainer";

const meta = {
  component: UserListContainer,

  // ここでは、Storyレベル で ハンドラ を設定しています。
  parameters: {
    msw: {
      handlers: [...handlers],
    },
  },

  // MockApolloProvider で Story を挟みます。
  decorators: [
    (Story: React.FC) => {
      return (
        <MockApolloProvider>
          <Story />
        </MockApolloProvider>
      );
    },
  ],
} satisfies Meta<typeof UserListContainer>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};
```


------------------------------------------------------------------------
### ① 必要なpackageの追加 と storybook設定ファイルの調整
- 必要なpackage追加 と storybook設定ファイルの調整 をします。

#### 以下コマンドで、 [MSW](https://github.com/mswjs/msw) と [msw-storybook-addon](https://github.com/mswjs/msw-storybook-addon) を追加

```zsh
$ npm i -D msw msw-storybook-addon
```

- [msw-storybook-addon](https://github.com/mswjs/msw-storybook-addon) は、Storyレベル で リクエストハンドラー を制御する際に便利なライブラリ です。
  - 分かりやすく機能するので本記事では導入していますが、「イーデス」では Storyレベルで MSW の handlerを定義する必要がない点と、不要な依存関係を増やさないという点で導入していません。

#### preview.ts(tsx) 内で、 msw-storybook-addon を利用するための記述 を追加
```ts
// .storybook/preview.ts

import type { Preview } from "@storybook/react";
+ import { initialize, mswLoader } from "msw-storybook-addon";

+ // Initialize MSW
+ initialize();

const preview: Preview = {
+ loaders: [mswLoader],

  parameters: {
    ...
  },
  
};

export default preview;
```

#### main.ts では staticDirs で public を指定しておく
```ts
// .storybook/main.ts

import type { StorybookConfig } from "@storybook/nextjs";

const config: StorybookConfig = {
  stories: ["../src/**/*.stories.@(ts|tsx)"],
  addons: [
    ...
  ],
  framework: {
    name: "@storybook/nextjs",
    options: {},
  },
  
+ staticDirs: ["../public"],
};

export default config;

```


---------------------------------------------------------------------------
### ② Service Worker のコードを生成
- MSW は、Service Worker を利用して APIリクエストをインターセプトするため、この `Service Worker のコード` を プロジェクトの公開（`public`）ディレクトリに追加する必要があります。

#### 以下コマンドで、 public/mockServiceWorker.js を追加
- ファイルは`mockServiceWorker.js` という名前で、コマンド実行 で 自動生成されます。
  - （--save オプションを指定しておくことで、(※1) の手間が省略されます。）

```zsh
$ npx msw init public/ --save
```

https://mswjs.io/docs/cli/init/


- (※1) --save オプション無しで コマンド実行したとしても、どのみち 以下の質問を Y で答えれば同じ設定をしてくれる。

```zsh
$ npx msw init public/

...

? Do you wish to save "public" as the worker directory?
Y
```

- また、コマンド実行後、package.json の末尾辺りに 以下が追記されます。
  - msw をプロジェクトの依存関係として管理するための記述です。
```package.json
"msw": {
  "workerDirectory": "public"
}
```


---------------------------------------------------------------------------
### ③ モック API の定義を追加
- 最後に、必要な設定 と モックの作成 をしていきます。

#### 開発環境でのみモックサーバーが起動されるよう、モッキング制御を追加
- この設定は、開発環境でのみモックサーバーを起動するためのものです。
- Next.js で MSWを使用する場合、通常は _app.tsx ファイルで設定が必要です。
- 以下では、NEXT_PUBLIC_API_MOCKING という環境変数が用意されている前提でモッキングを制御します。
-  `process.env.NODE_ENV === 'development'` などで判定するのも可。
```tsx
// src/pages/_app.tsx

if (process.env.NEXT_PUBLIC_API_MOCKING === "enabled") {
  require("../mocks");
}
```


#### プロジェクトの src 配下に mocks ディレクトリ を作成
- モックのためのファイルを作成していきます。

```zsh
$ mkdir src/mocks
```

#### mocks/index.ts を追加
- セットアップするためのファイルです。
- Next.js は ブラウザ と サーバーサイド の両方で実行されるため、それぞれの環境でセットアップが必要です。そのため、以下のような分岐を記述します。
```ts
// src/mocks/index.ts

if (typeof window === "undefined") {
  const { server } = require("./server");
  server.listen();
} else {
  const { worker } = require("./browser");
  worker.start();
}
export {};
```


#### mocks/browser.ts を追加
- ブラウザ環境向け に MSWのワーカー をセットアップするためのファイルです。
```ts
// src/mocks/browser.ts

import { setupWorker } from "msw";
import { handlers } from "./handlers";

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


#### mocks/server.ts を追加
- Node.js環境向け に MSWのサーバー をセットアップする ためのファイルです。
(主にテストで利用)
```ts
// src/mocks/server.ts

import { setupServer } from "msw/node";
import { handlers } from "./handlers";

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


#### mocks/MockApolloProvider.tsx を追加
- ApolloProvider をモックするためのファイルです。
- [ここ](https://qiita.com/drafts/2a29769c011002e3e62d#%E4%BE%8B%E3%81%A8%E3%81%97%E3%81%A6%E4%BB%A5%E4%B8%8B%E3%81%AE%E3%82%88%E3%81%86%E3%81%AA-userlist%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88-%E3%81%A8-%E3%81%9D%E3%81%AEstory%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB-%E3%82%92%E4%BD%9C%E6%88%90)で先出していますので、作成済みの場合はスルーしてください。

```tsx
// src/mocks/MockApolloProvider.tsx

import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client";

type MockApolloProviderProps = {
  children: React.ReactElement;
};

const client = new ApolloClient({
  uri: "fake.gql.server",
  cache: new InMemoryCache(),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: "no-cache",
      errorPolicy: "all",
    },
    query: {
      fetchPolicy: "no-cache",
      errorPolicy: "all",
    },
  },
});

export const MockApolloProvider = ({ children }: MockApolloProviderProps) => {
  return <ApolloProvider client={client}>{children}</ApolloProvider>;
};
```


#### handler.ts を追加
- MSW の ハンドラ定義 を記述するファイル例 です。
- MSW の 挙動が分かりやすくなるよう、あえて `delay(1000)` を付与しています。

```ts
// src/mocks/handler.ts

import { graphql } from "msw";
import { mockUsers } from "./users-data";

type GetUsersQuery = {
  users: {
    id: number;
    name: string;
    email: string;
  }[];
};

export const handlers = [
  graphql.query<GetUsersQuery>("GetUsers", (req, res, ctx) => {
    return res(
      ctx.delay(1000), // delay response time
      ctx.data({ users: mockUsers }) // mock users object from mocks/user-data.ts
    );
  }),
];
```


#### 以下コマンドで、 Storyの表示 を確認
- ここまでで準備ができたので、 Storybook上 で表示と挙動を確認

```zsh
$ npm run storybook
```

- Storybook の表示はこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534817/6a8ad550-2eca-bf38-819f-38db3c05759c.png)

- そして、Storybook上のブラウザの DevTools - Console を確認すると、以下のような表示が確認でき、MSWが機能していることが分かります。

```
[MSW] Mocking enabled.
```

- handler も、記述内容にミスが無く返ってきていれば、以下のような表示がされ、無事疎通していることが確認できます。
```
[MSW] xx:xx:xx query GetUsers (200 OK)
```

- 実際の表示と挙動 はこんな感じです。

![movie.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534817/ad95c46d-6aee-f24f-4d7e-21abb4b1beb7.gif)

- MSWにより、Story が表示できたので、無事 screenshot も可能になり、ショートコードのUI で VRTができるようになりました！


## まとめ
いかがだったでしょうか？
基本的に自分の備忘録用なのですが、誰かの役に立つ情報になっていれば幸いです。
ぜひ、MSWを利用して、APIに依存しないスムーズな開発をしていきましょう。

## 参考リンク
- [MSW - 公式ドキュメント](https://mswjs.io/)
- [MSW で加速するフロントエンド開発](https://zenn.dev/takepepe/articles/msw-driven-development)
- [Mock Service Workerを利用したGraphQL開発環境構築](https://zenn.dev/k_log24/articles/b944f8c03415fb)
- [Mock Service Worker で開発用のモックAPIを作る](https://zenn.dev/ryo_kawamata/articles/mock-api-server-with-msw)
- [Next.js で MSW をセットアップする方法](https://zenn.dev/akky1991/articles/37d03fc1b5f25e)