mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-08 11:11:53 +08:00
156 lines
8.0 KiB
Plaintext
156 lines
8.0 KiB
Plaintext
---
|
||
title: Mocking modules
|
||
sidebar:
|
||
order: 1
|
||
title: Modules
|
||
---
|
||
|
||
Components can also depend on modules that are imported into the component file. 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.
|
||
|
||
<Callout variant="info">
|
||
If you prefer learning by example, we created a [comprehensive demo project](https://github.com/storybookjs/module-mocking-demo) using the mocking strategies described here.
|
||
</Callout>
|
||
|
||
There are two primary approaches to mocking modules in Storybook. They both involve creating a mock file to replace the original module. The difference between the two approaches is how you import the mock file into your component.
|
||
|
||
For either approach, relative imports of the mocked module are not supported.
|
||
|
||
## 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`](https://vitest.dev/api/mock.html#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`:
|
||
|
||
{/* prettier-ignore-start */}
|
||
|
||
<CodeSnippets path="storybook-test-mock-file-example.md" />
|
||
|
||
{/* prettier-ignore-end */}
|
||
|
||
When you use the `fn` utility to mock a module, you create full [Vitest mock functions](https://vitest.dev/api/mock.html). See [below](#using-mocked-modules-in-stories) 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`](https://github.com/uuidjs/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:
|
||
|
||
```ts
|
||
// lib/uuid.ts
|
||
import { v4 } from 'uuid';
|
||
|
||
export const uuidv4 = v4;
|
||
```
|
||
|
||
And create a mock for the wrapper:
|
||
|
||
```ts
|
||
// lib/uuid.mock.ts
|
||
import { fn } from '@storybook/test';
|
||
|
||
import * as actual from './uuid';
|
||
|
||
export const uuidv4 = fn(actual.uuidv4).mockName('uuidv4');
|
||
```
|
||
|
||
## Subpath imports
|
||
|
||
The recommended method for mocking modules is to use [subpath imports](https://nodejs.org/api/packages.html#subpath-imports), a feature of Node packages that is supported by both [Vite](../../builders/vite.mdx) and [Webpack](../../builders/webpack.mdx).
|
||
|
||
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:
|
||
|
||
{/* prettier-ignore-start */}
|
||
|
||
<CodeSnippets path="subpath-imports-config.md" />
|
||
|
||
{/* prettier-ignore-end */}
|
||
|
||
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:
|
||
|
||
```ts
|
||
// AuthButton.ts
|
||
// ➖ Remove this line
|
||
// import { getUserFromSession } from '../../lib/session';
|
||
// ➕ Add this line
|
||
import { getUserFromSession } from '#lib/session';
|
||
|
||
// ... rest of the file
|
||
```
|
||
|
||
<Callout variant="info">
|
||
Subpath imports will only be correctly resolved and typed when the [`moduleResolution` property](https://www.typescriptlang.org/tsconfig/#moduleResolution) 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](https://www.totaltypescript.com/tsconfig-cheat-sheet) for guidance on setting up your TypeScript configuration.
|
||
</Callout>
|
||
|
||
## Builder aliases
|
||
|
||
If your project is unable to use [subpath imports](#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.
|
||
|
||
{/* prettier-ignore-start */}
|
||
|
||
<CodeSnippets path="module-aliases-config.md" />
|
||
|
||
{/* prettier-ignore-end */}
|
||
|
||
## Using mocked modules in stories
|
||
|
||
When you use the `fn` utility to mock a module, you create full [Vitest mock functions](https://vitest.dev/api/mock.html) which have many useful methods. For example, you can use the [`mockReturnValue`](https://vitest.dev/api/mock.html#mockreturnvalue) method to set a return value for the mocked function or [`mockImplementation`](https://vitest.dev/api/mock.html#mockimplementation) to define a custom implementation.
|
||
|
||
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:
|
||
|
||
{/* prettier-ignore-start */}
|
||
|
||
<CodeSnippets path="storybook-test-mock-return-value.md" />
|
||
|
||
{/* prettier-ignore-end */}
|
||
|
||
<Callout variant="info">
|
||
If you are [writing your stories in TypeScript](../typescript.mdx), 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](#subpath-imports) or [builder alias](#builder-aliases) is for.
|
||
</Callout>
|
||
|
||
### 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 [component tests](../../writing-tests/component-testing.mdx) 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:
|
||
|
||
{/* prettier-ignore-start */}
|
||
|
||
<CodeSnippets path="storybook-test-fn-mock-spy.md" />
|
||
|
||
{/* prettier-ignore-end */}
|
||
|
||
### 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.
|
||
|
||
<Callout variant="info">
|
||
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](../../api/parameters.mdx#restoremocks) for more information.
|
||
</Callout>
|
||
|
||
Here's an example of using the [`mockdate`](https://github.com/boblauer/MockDate) package to mock the [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) and reset it when the story unmounts.
|
||
|
||
{/* prettier-ignore-start */}
|
||
|
||
<CodeSnippets path="before-each-in-meta-mock-date.md" />
|
||
|
||
{/* prettier-ignore-end */}
|