こちらの記事はカケハシ Advent Calendar 2022 の 13 日目の記事になります。 https://adventar.org/calendars/7444
こんにちは。Musubi AI 在庫管理のフロントエンド開発を担当している鳥海です。
上記プロダクトのフロントエンドチームでは、私がチームにジョインした時(2022/9/1)と同時期から MSW が導入され、開発で用いるようになりました。 今回は、この MSW (とその周辺パッケージ)がすごく便利で感激したので、簡単にご紹介したいと思います。
この記事で紹介するもの
この記事では、下記項目についての説明をしていきたいと思います。 基本的に触りのみ紹介しますので、詳細についてはそれぞれのドキュメントをご確認いただけると幸いです。
開発フローと背景
MSW をご紹介する前に、なぜ必要になったのかについて、説明しようと思います。
開発フロー
フロントエンドチームでは、TypeScript × GraphQL を採用しており、次の手順で API 関連の開発を行なっています。
- スキーマの更新
- 更新されたスキーマから型を生成(GraphQL Code Generator を利用)
- アプリケーション内で API リクエストを行う処理を実装(Apollo Client を利用)
MSW が導入された背景
フロントエンドチームのメンバーも多くなり、開発がよりスピーディーになることで、バックエンドの実装のリリースタイミングを待たなくても良いようにしたいニーズが高まってきました。検討の結果、現在のフロントエンドの技術スタックと親和性の高い MSW が用いられることになりました。
MSW とは
MSW とは、Mock Service Worker の頭文字から来ているようで、Service Worker を起動し、設定した API リクエストをコールするたびに割り込みを行ってくれます。 また、REST, GraphQL の両方で同じようにモックしてくれるため、さまざまなプロジェクトで利用しやすいものとなっています(今回は GraphQL のプロジェクトなので、GraphQL での実装例の紹介となります)。
MSW の利用
MSW は、ブラウザとユニットテストそれぞれのユースケースで利用できます。 どちらの方法でも基本的に以下の方法で設定できます。
ブラウザで利用する場合の設定
mock/browser.ts
と mock/index.ts
に下記の設定を追加します。
// mock/browser.ts import { GraphQLHandler, setupWorker } from "msw"; const handlers: GraphQLHandler[] = []; export const worker = setupWorker(...handlers); // mock/index.ts async function initMocks() { if (typeof window !== "undefined") { const { worker } = await import("./browser"); worker.start({ onUnhandledRequest: "bypass", }); } }
※ initMocks
関数をローカル開発の時のみ実行されるように、アプリケーションの初期化時に実行される場所で処理を追加する必要があります
ユニットテストで利用する場合の設定
mock/server.ts
と .jest/setup.ts
に下記の設定を追加します。
// mock/server.ts import { GraphQLHandler, setupServer } from "msw/node"; const handlers: GraphQLHandler[] = []; export const server = setupServer(...handlers); // .jest/setup.ts import { server } from "../src/mocks/server"; beforeAll(() => { server.listen(); }); afterEach(() => { server.resetHandlers(); }); afterAll(() => server.close());
設定後にやること
上記設定をしたら handlers
部分に API の設定を追加していけば、MSW の設定は完了となります。( mock/browser.ts
or mock/server.ts
に変更を加えます。)
// mock/browser.ts or mock/server.ts const handlers: GraphQLHandler[] = [ mockXXQuery((req, res, ctx) => { return res( ctx.data({ xx: { key1: 'value1', key2: 'value2', } ); }),
下記のようなログが出てきたら、MSW の利用はできており、モック API が設定できている状態となっています。
比較的簡単に API モックを設定できたことが確認できたと思います。 テスト側の設定とブラウザ側の設定でこの程度であれば、導入がしやすくて、使い勝手が良さそうですね。
MSW に関連した便利なパッケージ集
このセクションでは、上記の MSW をさらに便利にしてくれるようなパッケージについて、いくつかご紹介したいと思います。(実のところ、ここからが感動した部分の大半です。)
graphql-codegen-typescript-mock-data
graphql-codegen-typescript-mock-data は、graphql-codegen での型の生成時に、graphql スキーマの定義に沿ったモックデータの生成関数を自動生成してくれる関数になります。
モック API に応用した使い方
利用方法は、まず codegen.yml
ファイルに以下のような設定を追記します。
# その他設定ファイルの続き src/mocks/data/generated.ts: plugins: - typescript-mock-data: typesFile: <path_to_graphql_type_file> prefix: fake addTypename: true dynamicValues: true generateLibrary: faker
その後、 graphql-codegen
のコマンドを実行すると、モックデータの生成関数が生成され、以下のように利用することができるようになります。
// mock/browser.ts import { setupWorker } from 'msw'; import { fakeXX, fakeYY } from "../../../../../mocks/data/generated"; const handlers: GraphQLHandler[] = [ mockXXQuery((req, res, ctx) => { // XX の型定義に沿ったモックデータを生成 return res( ctx.data({ xx: fakeXX(); ); }), mockYYQuery((req, res, ctx) => { // YY の型定義に沿ったモックデータに、自分で定義した値を override したデータを生成 return res( ctx.data({ yy: fakeYY({key1: '自分で定義した値'}), ); }), ]; export const worker = setupWorker(handlers);
このようにパッケージを導入すると、 fake
がついた関数を宣言するだけでモックデータを生成できるだけでなく、自分たちが定義した値を入れたい場合にも、値を override することで対応することもできます。
※ fake
の prefix で定義できるかは、codegen.yml
の設定ファイル次第です
テスト用の検証データとしての使い方
また、このパッケージについては、絶対に MSW の中で使わないといけないわけではないので、次のような使い方も可能です(実はこちらの方が利用している割合は多いです)。
// xx/index.test.ts import { fakeXX } from "../../../../../mocks/data/generated"; const xx1 = fakeXX(); describe('updateXX' () => { test('xx が更新されること', () => { expect(updateXX(xx1)).toStrictEqual(xx1); }) })
このようにテストファイルのダミーデータを作成するのに活用することもでき、色々なところでダミーデータを作成しやすくて便利です。
msw-storybook-addon
msw-storybook-addonは、Storybook で MSW を使用できるようにするための plugin です。MSW を利用できるようになるため、Storybook で API を利用しているコンポーネントの検証ができるようになります。
利用方法は、まず .storybook/preview.ts
のファイルで以下の設定を追記します。
import { initialize, mswDecorator } from "msw-storybook-addon"; initialize({ onUnhandledRequest: "bypass", }); export const decorators = [ (Story) => ( // ※ ApolloProvider の client には、適宜作成した client 設定を利用してください <ApolloProvider client={createStorybookApolloClient()}> <Story /> </ApolloProvider> ), mswDecorator, ];
あとは各 story ファイルに parameters
のキーに以下のように msw
の項目を追加すれば、 設定は完了です。この設定が完了すると、getXX
のクエリを API リクエストした時に、MSW がインターセプトして、モックデータを返してくれるようになります。
export default { parameters: { docs: { description: { component: "XX を表示するためのコンポーネント", }, }, msw: { handlers: [ mockGetXXQuery((req, res, ctx) => res( ctx.data({xx: fakeXX()}) ), ], }, }, };
これで、コンポーネント単位で挙動を確認したい場合にも、モック API を簡単に設定できるようになります。
2つのパッケージを導入して感じた良かった点と注意事項
これらのパッケージを導入したことで、いくつかの良かった点と注意事項が見えてきたので、整理したいと思います。
良かった点
- すべてのモックデータをベタ書きしているわけではないので、モックデータ作成時の効率・可読性が上がりました
- Storybook にも addon で MSW を利用できるようにしたため、プロダクトで共通して MSW をできるようになったため、API をモックする際は、ユニットテスト -> Storybook などのモックデータの使い回しなども可能になりました
(基本的に
handlers
で定義している部分をコピペすれば使い回せます)
注意事項
- graphql-codegen-typescript-mock-data のモックデータをランダムに生成する設定を
dynamicValues: true
にしておくと、テストなどや VRT でモックデータの値が都度変更されてしまうので、値を固定にしないと検証できないものの扱いは気をつける必要がある
まとめ
簡単にですが、 MSW とその周辺パッケージについてご紹介してみました。 今回ご紹介したものを組み合わせると、以下のようなさまざまな場面の対応ができると考えています。
- バックエンド対応が間に合わなく、先にフロントエンドの開発を進めたい場合
- モック API を利用したユニットテストを行いたい場合
- モック API を利用した Storybook での挙動確認を行いたい場合
また、モックデータを型定義にしたがって、関数 1 つで簡単に生成できるようになるので、全体的な開発効率・テストファイルの可読性も上がると思います。
このように、MSW と周辺パッケージを用いると、さまざまな用途で効率的に利用できるので、私は感激してしまいました。 この記事を読まれた皆様も一度試してみてはいかがでしょうか? ここまで読んでいただき、ありがとうございました。
最後に
カケハシでアドベントカレンダーやっています。他の記事もぜひ確認してみてください。