10
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ateam LifeDesignAdvent Calendar 2023

Day 22

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

Last updated at Posted at 2023-12-21

Ateam LifeDesign Advent Calendar 2023、Webデザイナーの@masaki632d が投稿します。

はじめに

私は現在、暮らしの「まよい」を解決する情報メディアの「イーデス」というサービスの開発を担当しています。

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

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

本記事で書かないこと
・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モックライブラリ です。
    • インターセプト: ググると直訳で「横取り・傍受」などという意味ですが、
      ここでは「通信やデータの流れを検知し、その内容を読み取る」という意味解釈が正しいですね。

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

MSW を利用するメリット

1. 実際のAPI を叩かないので サーバー負荷をかけない。 そして APIに依存せず開発が可能

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

2. GraphQL や REST のAPI に対応することができる

  • 今回は GraphQL で利用したいので言わずもがな。

3. Mockサーバ用のプロセス を別で立てる必要がない

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

MSW の使い方

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

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


⓪ (GraphQL環境 を用意)

  • 今回は、GraphQLを利用している環境でのユースケースなので、その環境を再現します。

以下コマンドで、 graphql@apollo/client を追加

$ npm i graphql @apollo/client
  • (※当方、パッケージマネージャーは yarn の1系 を使用しているのですが、Storybook v7 をインストールする際に 特有の依存関係エラーが発生したため、今回は npm を使用しています。)

例として、以下のような UserListコンポーネント と そのStoryファイル を作成

  • UserList コンポーネント を作成します。
// 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} />;
};
// 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>
  );
};
/* 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;
}
  • 先出しですが、モックファイルも作成しておきます。
// 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",
  },
];
// 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 の記述方法です。)
// 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設定ファイルの調整 をします。

以下コマンドで、 MSWmsw-storybook-addon を追加

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

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

// .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 を指定しておく

// .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) の手間が省略されます。)
$ npx msw init public/ --save

  • (※1) --save オプション無しで コマンド実行したとしても、どのみち 以下の質問を Y で答えれば同じ設定をしてくれる。
$ 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' などで判定するのも可。
// src/pages/_app.tsx

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

プロジェクトの src 配下に mocks ディレクトリ を作成

  • モックのためのファイルを作成していきます。
$ mkdir src/mocks

mocks/index.ts を追加

  • セットアップするためのファイルです。
  • Next.js は ブラウザ と サーバーサイド の両方で実行されるため、それぞれの環境でセットアップが必要です。そのため、以下のような分岐を記述します。
// 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のワーカー をセットアップするためのファイルです。
// src/mocks/browser.ts

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

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

mocks/server.ts を追加

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

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

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

mocks/MockApolloProvider.tsx を追加

  • ApolloProvider をモックするためのファイルです。
  • ここで先出していますので、作成済みの場合はスルーしてください。
// 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) を付与しています。
// 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上 で表示と挙動を確認
$ npm run storybook
  • Storybook の表示はこんな感じです。

image.png

  • そして、Storybook上のブラウザの DevTools - Console を確認すると、以下のような表示が確認でき、MSWが機能していることが分かります。
[MSW] Mocking enabled.
  • handler も、記述内容にミスが無く返ってきていれば、以下のような表示がされ、無事疎通していることが確認できます。
[MSW] xx:xx:xx query GetUsers (200 OK)
  • 実際の表示と挙動 はこんな感じです。

movie.gif

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

まとめ

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

参考リンク

10
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?