<aside> ℹ️

Storybook Test has launched and the full documentation is available here: https://storybook.js.org/docs/writing-tests

</aside>

One of the core principles of testing your code is that you only test what you intend to test. For example, consider a component which fetches data from an API then displays that data in a table. To only test that it renders correctly, you need to find a way to provide that data without requesting something from the network. Otherwise, you’re also testing whether or not the network is available and how stable the API is.

Network requests are one thing a component might depend on which must be considered. Other dependencies are imported modules or the context in which it is rendered. In all cases, we can mock these dependencies in our tests, to ensure consistency. Mocking also allows tests to manipulate a dependency, which can be useful when trying to test all permutations of a component.

Modules

The most common type of component dependency is the modules it imports, whether from external packages or internal to your project. When testing components, it can be helpful to mock these module dependencies and manipulate their behavior.

In Storybook, the preferred method to mock modules uses a Node.js standard called subpath imports, which is supported by much of the JS ecosystem, including TypeScript, Vite, and Webpack.

Subpath imports

<aside> ⚠️

If your project is unable to use subpath imports, you can use builder aliases instead.

</aside>

To configure subpath imports, you define the imports property in your project's package.json file. This property maps the subpath to the actual file path. The example below configures subpath imports for four internal modules:

// package.json
{
  "imports": {
    "#api": {
      // storybook condition applies to Storybook
      "storybook": "./api.mock.ts",
      "default": "./api.ts",
    },
    "#app/actions": {
      "storybook": "./app/actions.mock.ts",
      "default": "./app/actions.ts",
    },
    "#lib/session": {
      "storybook": "./lib/session.mock.ts",
      "default": "./lib/session.ts",
    },
    "#lib/db": {
      // test condition applies to test environments *and* Storybook
      "test": "./lib/db.mock.ts",
      "default": "./lib/db.ts",
    },
    "#*": ["./*", "./*.ts", "./*.tsx"],
  },
}

There are three aspects to this configuration worth noting:

First, each subpath must begin with #, to differentiate it from a regular module path. The #* entry is a catch-all that maps all subpaths to the root directory.

Second, the order of the keys is important. The default key should come last.

Third, note the storybooktest, and default keys in each module's entry. The storybook value is used to import the mock file when loaded in Storybook, while the default value is used to import the original module when loaded in your project. The test condition is also used within Storybook, which allows you to use the same configuration in Storybook and your other tests.

Notice that each of the storybook or test keys points to a *.mock.ts file. Those each look something like this:

// lib/session.mock.ts
import { fn } from '@storybook/test';
import * as actual from './session';

export * from './session';
export const getUserFromSession = fn(actual.getUserFromSession).mockName('getUserFromSession');

We import the original module and re-export that module’s functions wrapped with fn, which is a Vitest utility to mock and spy on functions.

<aside> ℹ️

You can't directly mock an external module like uuid or node:fs. Instead, you must wrap it in your own module, which you can mock like any other internal one. For example, with uuid, you could do the following:

// lib/uuid.ts
import { v4 } from 'uuid';
 
export const uuidv4 = v4;

Then you can create a mock as above for that wrapper.

</aside>

Using mocked modules in stories