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

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

Introduction - Mock Service Worker Docs

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

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

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

(本記事は Storybook 6.3.4, MSW 0.33.2 で動作を確認していますが、別のバージョンだと動作しない可能性があります。

その場合、msw-storybook-addon など別の方法で代替することができます。)

基本的な使い方

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

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

1
// src/mocks.js
2
import { setupWorker, rest } from 'msw'
3
4
const worker = setupWorker(
5
rest.post('/login', (req, res, ctx) => {
6
const isAuthenticated = sessionStorage.getItem('username')
7
8
if (!isAuthenticated) {
9
return res(
10
ctx.status(403),
11
ctx.json({
12
errorMessage: 'Not authenticated',
13
}),
14
)
15
}
16
17
return res(
18
ctx.json({
19
firstName: 'John',
20
}),
21
)
22
}),
23
)
24
25
// Register the Service Worker and enable the mocking
26
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 フラグを指定することで設定できます。

-s フラグは 6.4 から deprecated になりmain.js での設定に移行します。)

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

.storybook/main.js
1
module.exports = {
2
stories: [],
3
addons: [],
4
staticDirs: ['./public'],
5
}

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

package.json
1
{
2
"scripts": {
3
- "storybook": "start-storybook"
4
+ "storybook": "start-storybook -s .storybook/public"
5
}
6
}

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

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

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

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

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

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

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

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

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

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

global.d.ts
1
import type { SetupWorkerApi } from 'msw';
2
3
declare global {
4
interface Window {
5
msw: { worker: SetupWorkerApi };
6
}
7
}

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

以下は handlers の例です。

ComponentWithAPICall/mocks/handlers.ts
1
import { rest } from 'msw';
2
import type { GetResponse, GetResponseError } from '~/path/to/api/types';
3
4
const getUrl = '/api/path/to/get';
5
6
const getResponseExample: GetResponse = {
7
data: 'something',
8
};
9
10
const getResponseErrorExample: GetResponseError = {
11
message: 'failed',
12
};
13
14
export const handlers = {
15
default: rest.get(getUrl, (req, res, ctx) => {
16
return res(
17
ctx.delay(1000),
18
ctx.status(200),
19
ctx.json(getResponseExample)
20
);
21
}),
22
error: rest.get(getUrl, (req, res, ctx) => {
23
return res(
24
ctx.delay(1000),
25
ctx.status(500),
26
ctx.json(getResponseErrorExample)
27
);
28
}),
29
};

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

ComponentWithAPICall/ComponentWithAPICall.stories.tsx
1
import { handlers } from './mock/handlers';
2
3
const DefaultTemplate: ComponentStory<typeof ComponentWithAPICall> = (args) => {
4
const { worker } = window.msw;
5
6
useEffect(() => () => worker.resetHandlers());
7
8
worker.use(handlers.default);
9
10
return <ComponentWithAPICall {...args} />;
11
};
12
13
const ErrorTemplate: ComponentStory<typeof ComponentWithAPICall> = (args) => {
14
const { worker } = window.msw;
15
16
useEffect(() => () => worker.resetHandlers());
17
18
worker.use(handlers.error);
19
20
return <ComponentWithAPICall {...args} />;
21
};

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

ComponentWithAPICall/ComponentWithAPICall.test.tsx
1
import { setupServer } from 'msw/node';
2
import { handlers } from './mock/handlers';
3
4
const server = setupServer();
5
6
...
7
describe('when API request succeeded', () => {
8
beforeEach(() => server.resetHandlers(handlers.default));
9
...
10
})
11
12
describe('when API request failed', () => {
13
beforeEach(() => server.resetHandlers(handlers.error));
14
...
15
})

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

では 👋

参考

How to mock APIs in Storybook with MSW (Mock Service Worker) | by Alexis Oney | Medium