Mock Service Worker (MSW) + Storybook の設定手順と使用例
Mock Service Worker (MSW) とは、Service Woker が API リクエストを受け取って、レスポンスを返すことができる API mock ライブラリです。
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
に設定を追加するか、
module.exports = {
stories: [],
addons: [],
+ staticDirs: ['./public'],
}
package.json
の npm script を変更します。
{
"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
で設定します。
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 でエラーになってしまうので、まず型定義を追加しておきます。
import type { SetupWorkerApi } from 'msw';
declare global {
interface Window {
msw: { worker: SetupWorkerApi };
}
}
次に各コンポーネントに request handlers と stories ファイルを配置します。
以下は handlers の例です。
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 ファイルでの使用例は以下のようになります。
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
を使いまわせます。
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));
...
})
この記事が参考になったならうれしいです。
では 👋
参考
Discussion