k8o

LLMS

sb.mockでStorybookで利用するモジュールをモックしよう!

Storybook v9.1.0で新たに導入されたsb.mock機能により、モジュールのモックがこれまで以上に簡単に行えるようになりました。sb.mockはStorybookを活用したテストの取り組みやすさを大幅に向上させる画期的な新機能です。ぜひ活用してみてください。

公開: 2025年8月4日(月)
更新: 2025年8月4日(月)

はじめに

Storybookのv9.1.0でsb.mockを用いてモジュールをモックするAutomockingという機能が追加されました。

Loading...

Loading...

github.com

従来のStorybookで主流だった、サブパスインポートやViteWebpackのエイリアス機能を利用した手法を置き換えるような機能となっています(サブパスインポートを用いたモックについて)。

sb.mockを使ってみる

sbはオブジェクトで、storybook/testからインポートできます。

import { sb } from 'storybook/test';

v9.1.0時点ではmockメソッドのみが公開されています。

mockメソッドは第一引数にモックするモジュールのパス、第二引数にオプションをオブジェクトで指定します。

// オプション無し
sb.mock('./src/services/users');
// オプション有り
sb.mock('./src/services/users', {
  spy: true,
});

オプションはv9.1.0時点ではspyのみがサポートされています。

mockしてみる

sb.mockの第一引数に渡したモジュールをモックします。

// preview.ts
sb.mock('./src/services/users');

sb.mockはグローバルなモックなので定義可能な箇所はpreview.ts(x)のみであることに注意してください。

デフォルトの挙動

デフォルトの状態、またはspyをfalseに設定した場合、モックしたモジュールのメソッドを呼び出しても、そのメソッドの実際の処理は実行されません。

// preview.ts
sb.mock('./src/services/users', {
  spy: false,
});

// src/services/users.ts
console.log('これは呼ばれちゃいます');
export const getUser = () => {
  return { id: '1', name: 'John Doe' };
};

上記のように定義されている場合、Storyに表示するコンポーネント内でgetUserメソッドを呼び出しても、users.tsに定義された元のgetUserは実行されません。代わりにfn()が返されます。

ただし、元のメソッドが記述されたusers.tsファイル自体は読み込まれるので、例のconsole.logのようなグローバルに定義された宣言は実行されることに注意してください。

モックしたモジュールのメソッドをspyする

spytrueに設定すると、モックしたモジュールのメソッドを呼び出した際に、元の関数の処理が実際に実行されます。

// preview.ts
sb.mock('./src/services/users', {
  spy: true,
});

// src/services/users.ts
// この関数がそのまま使われる。
export const getUser = async (id: number) => {
  const user = await User.findById(id);
  if (!user) {
    return null;
  }

  return { id: user.id, name: user.name };
};

モックしたモジュールのメソッドはモック関数としても記録され、呼ばれた回数や引数を検証ができます。

モックしたモジュールのメソッドを検証する

デフォルトの挙動ではメソッドの戻り値がないため、何かしらの値を返すようにしたことが多いです。spyした場合であっても(そうでなくても)、検証のためにモックしたメソッドを制御したいです。

そのような場合はストーリーのbeforeEachmocked関数を用いて制御できます。

import { Meta, StoryObj } from '@storybook/react-vite';
import { expect, mocked } from 'storybook/test';
import { User } from './user';
import { getUser } from '@/services/users';

const meta = {
  title: 'user',
  component: User,
  beforeEach: async () => {
    mocked(getUser).mockResolvedValue({
      id: '1',
      name: 'John Doe',
    });
  },
} satisfies Meta<typeof User>;
export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
  play: async () => {
    expect(getUser).toHaveBeenCalledWith(1);
  },
};

export const NotFound: Story = {
  beforeEach: async () => {
    mocked(getUser).mockResolvedValue(null);
  },
  play: async ({ canvas, userEvent }) => {
    expect(getUser).toHaveBeenCalledWith(1);

    // ...
    // ...
    expect(user).toBeNull();
  },
};

storybook/testmocked関数にモックしたモジュールのメソッドを渡すことで、モックしたメソッドを制御できます。 StorybookのbeforeEachの単位で制御できるので便利ですね。

mocked関数はVitestMaybeMocked等を返すので、VitestMock Functionsで利用可能なすべてのメソッドを利用可能です。

別のファイルを呼び出す

sb.mockはデフォルトでモックしたモジュールのグローバルな宣言は実行されると前述しました。

サーバーでのみ動くような宣言がグローバルに含まれていたり、他のいくつかのケースではモック先のファイルを一切呼び出して欲しくないケースもあるでしょう。 その場合はモック専用のファイルを用意することで、モジュールごと置き換えられます。

手元のファイルのモック

src/services/users.tsのような手元のファイルをモックしたい場合は、src/services/__mocks__/users.tsのように同一の階層に__mocks__ディレクトリを作成し、その中にモックしたいファイルと同じ名前のファイルを作成します。

// src/services/users.ts
console.log('呼ばれなくなりました');
export const getUser = async (id: number) => {
  const user = await User.findById(id);
  if (!user) {
    return null;
  }

  return { id: user.id, name: user.name };
};

// src/services/__mocks__/users.ts
export const getUser = () => {
  return { id: '1', name: 'Mocked User' };
};
ライブラリのモック

node_modulesから呼び出すようなパッケージのモジュールをモックしたい場合は、プロジェクトのルートに__mocks__ディレクトリを作成し、その中にモックしたいファイルと同じ名前のファイルを作成します。

// __mocks__/react.ts
import { FC, PropsWithChildren } from 'react';

export const ViewTransition: FC<PropsWithChildren> = ({ children }) =>
  children;

プロジェクトのルートは.gitがある位置がデフォルトです。STORYBOOK_PROJECT_ROOT環境変数を設定することで、プロジェクトのルートを変更できます。

おわりに

従来のaliasを用いたモックでは、きめ細かい制御が難しく、思うような実装ができないことがありました。 サブパスインポートによるモックは詳細な制御が可能で便利でしたが、TypeScriptのpaths設定と競合し、インポートの解決でトラブルが多くストレスを感じる場面がありました。

新しく登場したsb.mockは、これらの問題を解決してくれます。現在のところ何のストレスもなく利用でき、まさに求めていた機能を手に入れることができました。

近年のStorybookはテスト機能が大きく充実し、これまで手が届かなかったテストも簡単に書けるようになるなど、開発者にとって非常に価値のある機能が次々と追加されています。

今後もStorybookのさらなる進化に注目していきたいです。

この記事はどうでしたか?

500文字以内でご記入ください

ブログの購読

k8oのブログを購読する

k8oのブログを購読することで、最新の情報を受け取ることができます。

登録いただいたメールアドレスは、購読のためにのみ使用されます。