Storybookではコンポーネントの計算を行い、その結果をStoryとして出力します。
いくつかのコンポーネントは外部のデータソースをもとに計算されます。
APIを用いたデータの取得はmswを用いたモックで一貫したStoryを提供できましたが、ormを用いたデータの取得や現在時刻に依存するコンポーネントはモックすることが難しくStorybookを利用するためだけに一工夫していました(Presenter層を作成するなど)。
Storybookの8.1からはNode.jsの機能であるSubpath Importsを用いてあらゆるデータのやり取りをモックできます。
Subpath Importsはpackage.json
を利用して環境に合わせてインポート先を変更できる機能です。
{
"imports": {
"#src/lib/kv": {
"storybook": "./src/lib/kv.mock.ts",
"default": "./src/lib/kv.ts"
},
"#*": "./*"
}
}
上記のように定義した状態で、#src/lib/kv
から読み込みを行うとstorybook環境では./src/lib/kv.mock.ts
を参照し、それ以外では./src/lib/kv.ts
を読み込むようになります。
// storybookからだけ代わりに./src/lib/kv.mock.tsを読み込む
// それ以外は./src/lib/kv.tsを読み込む
import { get } from '#src/lib/kv';
これを利用して行うのが今回紹介するモックです。
説明に利用するコードはStackBlitzに準備しました。
まずはモックする処理が記載されたファイルから紹介します。
import { kv } from '@vercel/kv';
export async function get(key: string): Promise<string | null> {
try {
const value = await kv.get<string>(key);
return value;
} catch (error) {
return null;
}
}
import { fn } from '@storybook/test';
import * as actual from './kv';
export const get = fn(actual.get).mockName('kv:get');
kv.ts
は@vercel/kv
のラッパーでVercel KVからデータを取得しています。このファイルをkv.mock.ts
でモックしています。
モックは@storybook/test
のfn
で対象の関数をベースにモックしています。モックが返す値は利用する側で決めます。fn(actual.get)
とすることでモックした関数に元の関数の型を継承することができます。
Storyの対象コンポーネントは以下のようになっています。
import { get } from '#src/lib/kv';
import { FC } from 'react';
export const Example: FC = async () => {
const value = await get('example');
if (!value) {
return <p>値がありません</p>;
}
return <p>{value}</p>;
};
処理を#src/lib/kv
からインポートすることで、普段の利用は./src/lib/kv.ts
、Storybookでは./src/lib/kv.mock.ts
を読み込みます。
Storyを定義するファイルは以下のとおりです。
import type { Meta, StoryObj } from '@storybook/react';
import { Example } from '#src/stories/example';
import { get } from '#src/lib/kv.mock';
const meta = {
component: Example,
} satisfies Meta<typeof Example>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default = {
beforeEach: () => {
get.mockResolvedValue('12');
},
} satisfies Story;
export const Empty = {
beforeEach: () => {
get.mockResolvedValue(null);
},
} satisfies Story;
StoryごとにbeforeEach
でモックが返す値を決めています。
この方法を使えば、どんな処理であっても別のファイルに切り出すことStorybook上で型安全にモックすることができます。