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設定ファイルの調整 をします。
以下コマンドで、 MSW と msw-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 をプロジェクトの依存関係として管理するための記述です。
"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 の表示はこんな感じです。
- そして、Storybook上のブラウザの DevTools - Console を確認すると、以下のような表示が確認でき、MSWが機能していることが分かります。
[MSW] Mocking enabled.
- handler も、記述内容にミスが無く返ってきていれば、以下のような表示がされ、無事疎通していることが確認できます。
[MSW] xx:xx:xx query GetUsers (200 OK)
- 実際の表示と挙動 はこんな感じです。
- MSWにより、Story が表示できたので、無事 screenshot も可能になり、ショートコードのUI で VRTができるようになりました!
まとめ
いかがだったでしょうか?
基本的に自分の備忘録用なのですが、誰かの役に立つ情報になっていれば幸いです。
ぜひ、MSWを利用して、APIに依存しないスムーズな開発をしていきましょう。