📕

Mock Service Worker (MSW) + Storybook の設定手順と使用例

2022/01/22に公開

Mock Service Worker (MSW) とは、Service Woker が API リクエストを受け取って、レスポンスを返すことができる API mock ライブラリです。

https://mswjs.io/docs/

MSW を使えば、Storybook とテストで共通の API mock handler を使用することができて便利です。

Storybook で MSW を使用するにあたっていくつか設定が必要になります。

調べると 1 コンポーネントしかないような小さな example は出てくるのですが、コンポーネント毎に mock を分けられるような方法にしたかったので、今回はその手順を記録しておきます。

基本的な使い方

Storybook との連携に入る前に、MSW の基本的な使い方について軽く触れておきます。

以下は公式ページに記載していたコード例です。

// src/mocks.js
import { setupWorker, rest } from 'msw'

const worker = setupWorker(
  rest.post('/login', (req, res, ctx) => {
    const isAuthenticated = sessionStorage.getItem('username')

    if (!isAuthenticated) {
      return res(
        ctx.status(403),
        ctx.json({
          errorMessage: 'Not authenticated',
        }),
      )
    }

    return res(
      ctx.json({
        firstName: 'John',
      }),
    )
  }),
)

// Register the Service Worker and enable the mocking
worker.start()

setupWorker() で Service Worker をセットアップし、worker.start() で立ち上げて、API mock を有効にします。

setupWorker() の引数にリクエスト形式、URL とレスポンス(request handlers)を指定します。

使用する際には npx msw init コマンドで mockServiceWorker.js を生成し、これを public ディレクトリなど、ルートパスから /mockServiceWorker.js としてアクセスできるディレクトリに配置する必要があります。

(これを知らずに 404 error を出して起動できないことがよくありました😇)

Storybook で使うための設定をする

ここから Storybook で MSW を使えるようにしていきます。

mockServiceWorker.js を生成

まずは必要となる mockServiceWorker.js を生成します。

.storybook/public/ ディレクトリを作成し、これを static directory に指定します。

プロジェクトルートで以下のコマンドを入力します。

npx msw init .storybook/public

すると .storybook/public/mockServiceWorker.js が生成されます。

Storybook の static directory を指定

Storybook においてルートパスからアクセスできるディレクトリ(static directory)は main.js において staticDirs を指定するか、-s フラグを指定することで設定できます。

.storybook/main.js に設定を追加するか、

.storybook/main.js
  module.exports = {
    stories: [],
    addons: [],
+   staticDirs: ['./public'],
  }

package.json の npm script を変更します。

package.json
  {
    "scripts": {
-     "storybook": "start-storybook"
+     "storybook": "start-storybook -s .storybook/public"
    }
  }

これで Storybook を立ち上げたときに mockServiceWorker.js が参照できるようになるはずです。

Storybook 起動時に Service Worker を立ち上げる

先述の通り、mock を開始するには setupWorker()worker.start() が必要です。

Storybook を立ち上げたときにすべての story に適用するため .storybook/preview.js で設定します。

.storybook/preview.js
import { setupWorker } from 'msw';

// Node 環境ではなくブラウザ環境にいることをチェック
if (typeof global.process === 'undefined') {
  // MSW をセットアップ
  const worker = setupWorker();
  // Service Worker を立ち上げる
  worker.start();
  // stories ファイルからアクセスできるように、worker をグローバルに参照できるようにする
  window.msw = { worker };
}

うまくいけば Storybook を立ち上げたときの console に [MSW] Mocking enabled. と表示されます。

現時点では setupWorker() の引数がないのですが、worker.use() を使うことで request handler を追加することができます。

MSW と Storybook 自体の設定はこれで完了です。

コンポーネントの stories ファイルで使う

TypeScript の場合、window.msw としてアクセスしたときに TypeScript でエラーになってしまうので、まず型定義を追加しておきます。

global.d.ts
import type { SetupWorkerApi } from 'msw';

declare global {
  interface Window {
    msw: { worker: SetupWorkerApi };
  }
}

次に各コンポーネントに request handlers と stories ファイルを配置します。

以下は handlers の例です。

ComponentWithAPICall/mocks/handlers.ts
import { rest } from 'msw';
import type { GetResponse, GetResponseError } from '~/path/to/api/types';

const getUrl = '/api/path/to/get';

const getResponseExample: GetResponse = {
  data: 'something',
};

const getResponseErrorExample: GetResponseError = {
  message: 'failed',
};

export const handlers = {
  default: rest.get(getUrl, (req, res, ctx) => {
    return res(
      ctx.delay(1000),
      ctx.status(200),
      ctx.json(getResponseExample)
    );
  }),
  error: rest.get(getUrl, (req, res, ctx) => {
    return res(
      ctx.delay(1000),
      ctx.status(500),
      ctx.json(getResponseErrorExample)
    );
  }),
};

stories ファイルでの使用例は以下のようになります。

ComponentWithAPICall/ComponentWithAPICall.stories.tsx
import { handlers } from './mock/handlers';

const DefaultTemplate: ComponentStory<typeof ComponentWithAPICall> = (args) => {
  const { worker } = window.msw;

  useEffect(() => () => worker.resetHandlers());

  worker.use(handlers.default);

  return <ComponentWithAPICall {...args} />;
};

const ErrorTemplate: ComponentStory<typeof ComponentWithAPICall> = (args) => {
  const { worker } = window.msw;

  useEffect(() => () => worker.resetHandlers());

  worker.use(handlers.error);

  return <ComponentWithAPICall {...args} />;
};

テストのときも同じ handlers を使いまわせます。

ComponentWithAPICall/ComponentWithAPICall.test.tsx
import { setupServer } from 'msw/node';
import { handlers } from './mock/handlers';

const server = setupServer();

...
describe('when API request succeeded', () => {
  beforeEach(() => server.resetHandlers(handlers.default));
  ...
})

describe('when API request failed', () => {
  beforeEach(() => server.resetHandlers(handlers.error));
  ...
})

この記事が参考になったならうれしいです。

では 👋

参考

https://alexis-oney.medium.com/how-to-mock-apis-in-storybook-with-msw-mock-service-worker-c0c5c91e6c9c

Discussion