Mocking modules
Components often depend on other modules, such as other components, utility functions, or libraries. These can be from external packages or internal to your project. When rendering those components in Storybook or testing them, you may want to mock those modules to control their behavior and isolate the component's functionality.
For example, this simple component depends on two modules, a local utility function to access the user's browser session and an external package to generate a unique ID:
import { v4 as uuidv4 } from 'uuid';
import { getUserFromSession } from '../lib/session';
export function AuthButton() {
const user = getUserFromSession();
const id = uuidv4();
return (
<button onClick={() => { console.log(`User: ${user.name}, ID: ${id}`) }}>
{user ? `Welcome, ${user.name}` : 'Sign in'}
</button>
);
}
The above example is written with React, but the same principles apply to other renderers like Vue, Svelte, or Web Components. The important part is the usage of the two module dependencies.
When writing stories or tests for this component, you may want to mock the getUserFromSession
function to control the user data returned, or mock the uuidv4
function to return a predictable ID. This allows you to test the component's behavior without relying on the actual implementations of these modules.
For maximum flexibility, Storybook provides three ways to mock modules for your stories. Let's walk through each of them, starting with the most straightforward approach.
Automocking
Automocking is the most straightforward way to mock modules in Storybook, and we recommend it for all projects using the Vite and Webpack builders (other builders must use one of the other techniques, below). This approach requires minimal configuration while allowing for flexible mocking of modules.
It works with two steps. First, register the modules you want to mock in your Storybook configuration. Then, control the behavior and make assertions about the mocked modules in your stories.
Registering modules to mock
When automocking, you use the sb.mock
utility function to register modules you want to mock. There are three ways to register modules: as spy-only, fully automocked, or with a mock file. Each method has its use cases and benefits.
There are some key details to keep in mind when using the sb.mock
utility:
- You can register both local modules (e.g.,
../lib/session.ts
) and packages innode_modules
(e.g.,uuid
). - You can only register mocked modules in your project-level configuration:
.storybook/preview.js|ts
. This ensures consistent and performant mocking across all stories in your project. You can modify the behavior of these modules in your stories, but you cannot register them directly in the story files. - When registering a mock for a local module, the path must:
- Not use an alias or subpath import (e.g.,
@/lib/session.ts
or#lib/session
). - Be relative to the
.storybook/preview.js|ts
file. - Include the file extension (e.g.,
.ts
or.js
).
- Not use an alias or subpath import (e.g.,
- If you are using Typescript, you can wrap the module path in
import()
to ensure the module is correctly resolved and typed. For example,sb.mock(import('../lib/session.ts'))
. - If you are using the Webpack builder, you can only automock
node_module
packages that have ESModules (ESM) entry points. If a module has both CommonJS (CJS) and ESM entry points, Webpack doesn't correctly resolve the ESM entry and it cannot be mocked. Webpack users can still mock CJSnode_module
packages by providing a mock file.
Spy-only
For most cases, you should register a mocked module as spy-only, by setting the spy
option to true
. This leaves the original module's functionality intact, while still allowing you to modify the behavior if needed and make assertions in your tests.
For example, if you want to spy on the getUserFromSession
function and the uuidv4
function from the uuid
package, you can call the sb.mock
utility function in your .storybook/preview.js|ts
file:
import { sb } from 'storybook/test';
// ๐ Automatically spies on all exports from the `lib/session` local module
sb.mock(import('../lib/session.ts'), { spy: true });
// ๐ Automatically spies on all exports from the `uuid` package in `node_modules`
sb.mock(import('uuid'), { spy: true });
// ...rest of the file
If you need to mock an external module that has a deeper import path (e.g. lodash-es/add
), register the mock with that path.
You can then control the behavior of these modules and make assertions about them in your stories, such as checking if a function was called or what arguments it was called with.
Fully automocked modules
For cases where you need to prevent the original module's functionality from executing, set the spy
option to false
(or omit it, because that is the default value). This will automatically replace all exports from the module with Vitest mock functions, allowing you to control their behavior and make assertions while being certain that the original functionality never runs.
import { sb } from 'storybook/test';
// ๐ Automatically replaces all exports from the `lib/session` local module with mock functions
sb.mock(import('../lib/session.ts'));
// ๐ Automatically replaces all exports from the `uuid` package in `node_modules` with mock functions
sb.mock(import('uuid'));
// ...rest of the file
Fully automocked modules do not execute their exported functions, but the module is still evaluated, along with its dependencies. This means that if the module has side effects (e.g., modifying global state, logging to the console, etc.), those side effects will still occur. Similarly, a module written to run on the server will attempt to be evaluated in the browser. If you want to prevent the original module's code from running entirely, you should use a mock file instead.
You can then control the behavior of these modules and make assertions about them in your stories, just like with the spy-only approach.
Mock files
If you want to mock a module with more complex behavior or reuse a mock's behavior across multiple stories, you can create a mock file. This file should be placed in a __mocks__
directory next to the module you want to mock, and it should export the same named exports as the original module.
For example, to mock the session
module in the lib
directory, create a file named session.js|ts
in the lib/__mocks__
directory:
export function getUserFromSession() {
return { name: 'Mocked User' };
}
For packages in your node_modules
, create a __mocks__
directory in the root of your project and create the mock file there. For example, to mock the uuid
package, create a file named uuid.js
in the __mocks__
directory:
export function v4() {
return '1234-5678-90ab-cdef';
}
If you need to mock an external module that has a deeper import path (e.g. lodash-es/add
), create a corresponding mock file (e.g. __mocks__/lodash-es/add.js
) in the root of your project.
The root of your project is determined differently depending on your builder:
Vite projects
The root __mocks__
directory should be placed in the root
directory, as defined in your project's Vite configuration (typically process.cwd()
) If that is unavailable, it defaults to the directory containing your .storybook
directory.
Webpack projects
The root __mocks__
directory should be placed in the context
directory, as defined in your project's Webpack configuration (typically process.cwd()
). If that is unavailable, it defaults to the root of your repository.
Mock files must be written with JavaScript (not TypeScript) using ESModules (not CJS).
They must export the same named exports as the original module. If you want to mock a default export, you can use export default
in the mock file.
You can then use the sb.mock
utility to register these mock files in your preview.js|ts
file:
import { sb } from 'storybook/test';
// ๐ Replaces imports of this module with imports to `../lib/__mocks__/session.ts`
sb.mock(import('../lib/session.ts'));
// ๐ Replaces imports of this module with imports to `../__mocks__/uuid.ts`
sb.mock(import('uuid'));
// ...rest of the file
Note that the API for registering automatically mocked modules and mock files is the same. The only difference is that sb.mock
will first look for a mock file in the appropriate directory before automatically mocking the module.
Using automocked modules in stories
All registered automocked modules are used the same way within your stories. You can control the behavior, such as defining what it returns, and make assertions about the modules.
// Replace your-framework with the name of your framework (e.g. react-vite, vue3-vite, etc.)
import type { Meta, StoryObj } from '@storybook/your-framework';
import { expect, mocked } from 'storybook/test';
import { AuthButton } from './AuthButton';
import { v4 as uuidv4 } from 'uuid';
import { getUserFromSession } from '../lib/session';
const meta = {
component: AuthButton,
// ๐ This will run before each story is rendered
beforeEach: async () => {
// ๐ Force known, consistent behavior for mocked modules
mocked(uuidv4).mockReturnValue('1234-5678-90ab-cdef');
mocked(getUserFromSession).mockReturnValue({ name: 'John Doe' });
},
} satisfies Meta<typeof AuthButton>;
export default meta;
type Story = StoryObj<typeof meta>;
export const LogIn: Story = {
play: async ({ canvas, userEvent }) => {
const button = canvas.getByRole('button', { name: 'Sign in' });
userEvent.click(button);
// Assert that the getUserFromSession function was called
expect(getUserFromSession).toHaveBeenCalled();
},
};
Mocked functions created with the sb.mock
utility are full Vitest mock functions, which means you can use all the methods available on them. Some of the most useful methods include:
Method | Description |
---|---|
mockReturnValue(value) | Sets the return value of the mocked function. |
mockResolvedValue(value) | Sets the value the mocked async function resolves to. |
mockImplementation(fn) | Sets a custom implementation for the mocked function. |
If you are writing your stories in TypeScript, you can use the mocked
utility from storybook/test
to ensure that the mocked functions are correctly typed in your stories. This utility is a type-safe wrapper around the Vitest vi.mocked
function.
How it works
Storybook's automocking is built on Vitest's mocking engine. The behavior adjusts depending on whether you're in development mode or build mode:
Dev Mode
In dev mode, mocking relies on Vite's module graph invalidation. When a mock is added, changed, or removed (either in .storybook/preview.js|ts
or the __mocks__
directory), the plugin intelligently invalidates all affected modules and triggers a hot reload. This provides a fast and interactive development experience.
Dev and build mode
- Build-time analysis: A new Vite plugin, viteMockPlugin, scans
.storybook/preview.js|ts
for allsb.mock()
calls during the build process. - Mock Processing:
__mocks__
redirects: If a corresponding file is found in the top-level__mocks__
directory, that file is loaded and transformed by Vite.- Automocking & spies: If no
__mocks__
file is found, the original module's code is transformed at build-time to replace its exports with mocks or spies.
- No runtime overhead: Because all mocking decisions and transformations happen at build time, there is no performance penalty or complex interception logic needed in the final built application. The mocked modules are directly bundled in place of the originals.
Comparison to Vitest mocking
While this feature uses Vitest's mocking engine, the implementation within Storybook has some key differences:
- Scope: Mocks are global and defined only in
.storybook/preview.js|ts
. Unlike Vitest, you cannot callsb.mock()
inside individual story files. - Static by Design: All mocking decisions are finalized at build time. This makes the system robust and performant but less dynamic than Vitest's test-by-test mocking capabilities. There is no
sb.unmock()
or equivalent, as the module graph is fixed in a production build. - Runtime Mocking: While the module swap is static, you can still control the behavior of the mocked functions at runtime within a play function or
beforeEach
hook (e.g.,mocked(myFunction).mockReturnValue('new value')
). - No Factory Functions: The
sb.mock()
API does not accept a factory function as its second argument (e.g.,sb.mock('path', () => ({...}))
). This is because all mocking decisions are resolved at build time, whereas factories are executed at runtime.
Alternative methods
If automocking is not suitable for your project, there are two alternative methods to mock modules in Storybook: subpath imports and builder aliases. These methods require a bit more setup but provide similar functionality to automocking, allowing you to control the behavior of modules in your stories.
Subpath imports
You can use subpath imports, a Node feature, to mock modules. Subpath imports allow you to define custom paths for modules in your project, which can be used to replace the original module with a mock file. They work with both the Vite and Webpack builders.
Mock files
To mock a module, create a file with the same name and in the same directory as the module you want to mock. For example, to mock a module named session
, create a file next to it named session.mock.js|ts
, with a few characteristics:
- It must import the original module using a relative import.
- Using a subpath or alias import would result in it importing itself.
- It should re-export all exports from the original module.
- It should use the
fn
utility to mock any necessary functionality from the original module. - It should use the
mockName
method to ensure the name is preserved when minified - It should not introduce side effects that could affect other tests or components. Mock files should be isolated and only affect the module they are mocking.
Here's an example of a mock file for a module named session
:
import { fn } from 'storybook/test';
import * as actual from './session';
export * from './session';
export const getUserFromSession = fn(actual.getUserFromSession).mockName('getUserFromSession');
When you use the fn
utility to mock a module, you create full Vitest mock functions. See below for examples of how you can use a mocked module in your stories.
Mock files for external modules
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:
import { v4 } from 'uuid';
export const uuidv4 = v4;
And create a mock for the wrapper:
import { fn } from 'storybook/test';
import * as actual from './uuid';
export const uuidv4 = fn(actual.uuidv4).mockName('uuidv4');
Configuration
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:
{
"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 storybook
, test
, 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.
With the package configuration in place, you can then update your component file to use the subpath import:
// โ Remove this line
// import { getUserFromSession } from '../../lib/session';
// โ Add this line
import { getUserFromSession } from '#lib/session';
// ...rest of the file
Subpath imports will only be correctly resolved and typed when the moduleResolution
property is set to 'Bundler'
, 'NodeNext'
, or 'Node16'
in your TypeScript configuration.
If you are currently using 'node'
, that is intended for projects using a Node.js version older than v10. Projects written with modern code likely do not need to use 'node'
.
Storybook recommends the TSConfig Cheat Sheet for guidance on setting up your TypeScript configuration.
Using subpath imports in stories
When you use the fn
utility to mock a module, you create full Vitest mock functions, which have many methods available. Some of the most useful methods include:
Method | Description |
---|---|
mockReturnValue(value) | Sets the return value of the mocked function. |
mockResolvedValue(value) | Sets the value the mocked async function resolves to. |
mockImplementation(fn) | Sets a custom implementation for the mocked function. |
Here, we define beforeEach
on a story (which will run before the story is rendered) to set a mocked return value for the getUserFromSession
function used by the Page component:
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import { mocked } from 'storybook/test';
// ๐ Automocked module resolves to '../lib/__mocks__/session'
import { getUserFromSession } from '../lib/session';
import { Page } from './Page';
const meta = {
component: Page,
} satisfies Meta<typeof Page>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
async beforeEach() {
// ๐ Set the return value for the getUserFromSession function
mocked(getUserFromSession).mockReturnValue({ id: '1', name: 'Alice' });
},
};
If you are writing your stories in TypeScript, you must import your mock modules using the full mocked file name to have the functions correctly typed in your stories. You do not need to do this in your component files. That's what the subpath import or builder alias is for.
Spying on mocked modules
The fn
utility also spies on the original module's functions, which you can use to assert their behavior in your tests. For example, you can use interaction tests to verify that a function was called with specific arguments.
For example, this story checks that the saveNote
function was called when the user clicks the save button:
// Replace your-framework with svelte-vite or sveltekit
import type { Meta, StoryObj } from '@storybook/your-framework';
import { expect } from 'storybook/test';
// ๐ Automocked module resolves to '../app/__mocks__/actions'
import { saveNote } from '../app/actions';
import { createNotes } from '../app/mocks/notes';
import NoteUI from './note-ui.svelte';
const meta = {
title: 'Mocked/NoteUI',
component: NoteUI,
} satisfies Meta<typeof NoteUI>;
export default meta;
type Story = StoryObj<typeof meta>;
const notes = createNotes();
export const SaveFlow: Story = {
name: 'Save Flow โถ',
args: {
isEditing: true,
note: notes[0],
},
play: async ({ canvas, userEvent }) => {
const saveButton = canvas.getByRole('menuitem', { name: /done/i });
await userEvent.click(saveButton);
// ๐ This is the mock function, so you can assert its behavior
await expect(saveNote).toHaveBeenCalled();
},
};
Builder aliases
If your project is unable to use automocking or subpath imports, you can configure your Storybook builder to alias the module to the mock file. This will instruct the builder to replace the module with the mock file when bundling your Storybook stories.
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs-vite, vue3-vite, etc.
import type { StorybookConfig } from '@storybook/your-framework';
const config: StorybookConfig = {
framework: '@storybook/your-framework',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
viteFinal: async (config) => {
if (config.resolve) {
config.resolve.alias = {
...config.resolve?.alias,
// ๐ External module
lodash: require.resolve('./lodash.mock'),
// ๐ Internal modules
'@/api': path.resolve(__dirname, './api.mock.ts'),
'@/app/actions': path.resolve(__dirname, './app/actions.mock.ts'),
'@/lib/session': path.resolve(__dirname, './lib/session.mock.ts'),
'@/lib/db': path.resolve(__dirname, './lib/db.mock.ts'),
};
}
return config;
},
};
export default config;
Usage of the aliased module in stories is similar to when using subpath imports in stories, but you import the module using the alias instead of the subpath.
Common scenarios
Setting up and cleaning up
Before the story renders, you can use the asynchronous beforeEach
function to perform any setup you need (e.g., configure the mock behavior). This function can be defined at the story, component (which will run for all stories in the file), or project (definedย in .storybook/preview.js|ts
,ย which will run for all stories in the project).
You can also return a cleanup function from beforeEach
which will be called after your story unmounts. This is useful for tasks like unsubscribing observers, etc.
It is not necessary to restore fn()
mocks with the cleanup function, as Storybook will already do that automatically before rendering a story. See the parameters.test.restoreMocks
API for more information.
Here's an example of using the mockdate
package to mock the Date
and reset it when the story unmounts.
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import MockDate from 'mockdate';
import { Page } from './Page';
const meta = {
component: Page,
// ๐ Set the value of Date for every story in the file
async beforeEach() {
MockDate.set('2024-02-14');
// ๐ Reset the Date after each story
return () => {
MockDate.reset();
};
},
} satisfies Meta<typeof Page>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
async play({ canvas }) {
// ... This will run with the mocked Date
},
};
Troubleshooting
Receiving an exports is not defined
error
Webpack projects may encounter an exports is not defined
error when using automocking. This is usually caused by attempting to mock a module with CommonJS (CJS) entry points. Automocking with Webpack only works with modules that have ESModules (ESM) entry points exclusively, so you must use a mock file to mock CJS modules.