模拟模块
组件通常依赖于其他模块,例如其他组件、实用函数或库。这些可以来自外部包或项目内部。在 Storybook 或 testing 中渲染这些组件时,你可能需要模拟这些模块以控制它们的行为并隔离组件的功能。
¥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.
例如,这个简单的组件依赖于两个模块:一个用于访问用户浏览器会话的本地实用程序函数和一个用于生成唯一 ID 的外部包:
¥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>
);
}上述示例是用 React 编写的,但相同的原则也适用于其他渲染器,例如 Vue、Svelte 或 Web Components。关键在于两个模块依赖的使用。
¥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.
在为此组件编写故事或测试时,你可能需要模拟 getUserFromSession 函数来控制返回的用户数据,或者模拟 uuidv4 函数以返回可预测的 ID。这允许你测试组件的行为,而无需依赖这些模块的实际实现。
¥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.
为了实现最大的灵活性,Storybook 提供了三种模拟故事模块的方法。让我们逐一介绍一下这些方法,从最简单的方法开始。
¥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
自动模拟是在 Storybook 中模拟模块最直接的方法,我们建议所有使用 Vite 和 Webpack 构建器的项目都使用它(其他构建器必须使用以下其他技术之一)。这种方法需要最少的配置,同时允许灵活地模拟模块。
¥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.
它分为两个步骤。首先,在你的 Storybook 配置中注册你想要模拟的模块。然后,控制行为并对故事中模拟的模块进行断言。
¥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
自动模拟时,你可以使用 sb.mock 实用程序函数来注册要模拟的模块。注册模块有三种方法:以 spy-only、完全自动模拟或使用模拟文件的形式。每种方法都有其用例和优势。
¥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.
使用 sb.mock 实用程序时,需要注意一些关键细节:
¥There are some key details to keep in mind when using the sb.mock utility:
-
你可以注册本地模块(例如
../lib/session.ts)和node_modules中的包(例如uuid)。¥You can register both local modules (e.g.,
../lib/session.ts) and packages innode_modules(e.g.,uuid). -
你只能在项目级配置中注册模拟模块:
.storybook/preview.js|ts。这可确保项目中所有故事的模拟一致且性能良好。你可以在故事中修改这些模块的行为,但不能直接在故事文件中注册它们。¥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:
-
不要使用别名或子路径导入(例如
@/lib/session.ts或#lib/session)。¥Not use an alias or subpath import (e.g.,
@/lib/session.tsor#lib/session). -
与
.storybook/preview.js|ts文件相关。¥Be relative to the
.storybook/preview.js|tsfile. -
包含文件扩展名(例如,
.ts或.js)。¥Include the file extension (e.g.,
.tsor.js).
-
-
如果你使用的是 Typescript,你可以将模块路径封装在
import()中,以确保模块被正确解析和输入。例如,sb.mock(import('../lib/session.ts'))。¥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')). -
如果你使用的是 Webpack 构建器,则只能自动模拟具有 ESModules (ESM) 入口点的
node_module包。如果模块同时具有 CommonJS (CJS) 和 ESM 入口点,则 Webpack 无法正确解析 ESM 入口,因此无法模拟。Webpack 用户仍然可以通过提供 模拟文件 来模拟 CJSnode_module包。¥If you are using the Webpack builder, you can only automock
node_modulepackages 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_modulepackages by providing a mock file.
仅用于监视
¥Spy-only
在大多数情况下,你应该通过将 spy 选项设置为 true,将模拟模块注册为仅间谍模块。这可以保持原始模块的功能不变,同时仍然允许你根据需要修改行为并在测试中进行断言。
¥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.
例如,如果你想监视 uuid 包中的 getUserFromSession 函数和 uuidv4 函数,你可以在 .storybook/preview.js|ts 文件中调用 sb.mock 实用程序函数:
¥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如果你需要模拟具有更深导入路径的外部模块(例如 lodash-es/add),请使用该路径注册该模拟文件。
¥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
如果你需要阻止原始模块功能的执行,请将 spy 选项设置为 false(或者忽略它,因为这是默认值)。这将自动用 Vitest 模拟函数 替换模块中的所有导出,让你可以控制它们的行为并进行断言,同时确保原始功能永远不会运行。
¥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
如果你想模拟具有更复杂行为的模块或在多个故事中重用模拟行为,你可以创建一个模拟文件。此文件应放置在你要模拟的模块旁边的 __mocks__ 目录中,并且它应导出与原始模块相同的名称。
¥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.
例如,要模拟 lib 目录中的 session 模块,请在 lib/__mocks__ 目录中创建一个名为 session.js|ts 的文件:
¥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' };
}对于 node_modules 中的软件包,请在项目根目录中创建一个 __mocks__ 目录,并在其中创建模拟文件。例如,要模拟 uuid 包,请在 __mocks__ 目录中创建一个名为 uuid.js 的文件:
¥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';
}如果你需要模拟具有更深导入路径的外部模块(例如 lodash-es/add),请在项目根目录中创建相应的模拟文件(例如 __mocks__/lodash-es/add.js)。
¥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 项目
¥Vite projects
根目录 __mocks__ 应放置在项目 Vite 配置中定义的 root 目录 目录下(通常为 process.cwd())。如果该目录不可用,则默认为包含 .storybook 目录的目录。
¥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 项目
¥Webpack projects
根目录 __mocks__ 应放置在项目 Webpack 配置中定义的 context 目录 目录下(通常为 process.cwd())。如果最新版本不可用,则默认安装到你的代码库的根目录。
¥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.
模拟文件必须使用 JavaScript(而非 TypeScript)和 ESModules(而非 CJS)编写。
¥Mock files must be written with JavaScript (not TypeScript) using ESModules (not CJS).
它们必须导出与原始模块相同的命名导出。如果你想模拟默认导出,你可以在模拟文件中使用 export default。
¥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.
然后,你可以使用 sb.mock 实用程序在 preview.js|ts 文件中注册这些模拟文件:
¥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请注意,用于自动注册模拟模块和模拟文件的 API 是相同的。唯一的区别是,sb.mock 会先在相应的目录中查找模拟文件,然后再自动模拟模块。
¥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();
},
};使用 sb.mock 实用程序创建的模拟函数是完整的 Vitest 模拟函数 版本,这意味着你可以使用它们上所有可用的方法。一些最有用的方法包括:
¥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:
| 方法 | 描述 |
|---|---|
mockReturnValue(value) | 设置模拟函数的返回值。 |
mockResolvedValue(value) | 设置模拟异步函数解析后的值。 |
mockImplementation(fn) | 为模拟函数设置自定义实现。 |
如果你是 用 TypeScript 写你的故事,你可以使用 storybook/test 中的 mocked 实用程序来确保模拟函数在你的故事中正确输入。此实用程序是 Vitest vi.mocked 函数的类型安全封装器。
¥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 的自动模拟功能基于 Vitest 的模拟引擎构建。行为会根据你处于开发模式还是构建模式进行调整:
¥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
在开发模式下,模拟依赖于 Vite 的模块图失效机制。当添加、更改或删除模拟(无论是在 .storybook/preview.js|ts 还是 __mocks__ 目录中)时,该插件会智能地使所有受影响的模块失效并触发热重载。这提供了快速且交互式的开发体验。
¥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
-
构建时分析:新的 Vite 插件 viteMockPlugin 会在构建过程中扫描
.storybook/preview.js|ts中所有sb.mock()调用。¥Build-time analysis: A new Vite plugin, viteMockPlugin, scans
.storybook/preview.js|tsfor allsb.mock()calls during the build process. -
模拟处理:
¥Mock Processing:
-
__mocks__重定向:如果在顶层__mocks__目录中找到相应的文件,则该文件将由 Vite 加载和转换。¥
__mocks__redirects: If a corresponding file is found in the top-level__mocks__directory, that file is loaded and transformed by Vite. -
自动模拟和间谍:如果未找到
__mocks__文件,则原始模块的代码将在构建时进行转换,用 mocks 或 spies 替换其导出内容。¥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.
与 Vitest mocking 的比较
¥Comparison to Vitest mocking
虽然此功能使用了 Vitest 的模拟引擎,但在 Storybook 中的实现存在一些关键差异:
¥While this feature uses Vitest's mocking engine, the implementation within Storybook has some key differences:
-
范围:模拟是全局的,仅在
.storybook/preview.js|ts中定义。与 Vitest 不同,你无法在单个 Story 文件中调用sb.mock()。¥Scope: Mocks are global and defined only in
.storybook/preview.js|ts. Unlike Vitest, you cannot callsb.mock()inside individual story files. -
静态设计:所有模拟决策均在构建时最终确定。这使得系统健壮且性能出色,但动态性不如 Vitest 的逐个测试模拟功能。没有
sb.unmock()或等效版本,因为模块图在生产版本中是固定的。¥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. -
运行时模拟:虽然模块交换是静态的,但你仍然可以在 play 函数或
beforeEach钩子(例如mocked(myFunction).mockReturnValue('new value'))中控制模拟函数在运行时的行为。¥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
beforeEachhook (e.g.,mocked(myFunction).mockReturnValue('new value')). -
无工厂函数:
sb.mock()API 不接受工厂函数作为其第二个参数(例如sb.mock('path', () => ({...})))。这是因为所有模拟决策都在构建时解析,而工厂方法则在运行时执行。¥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
如果 automocking 不适合你的项目,有两种替代方法可以在 Storybook 中模拟模块:子路径导入 和 构建器别名。这些方法需要更多设置,但提供与自动模拟类似的功能,允许你控制故事中模块的行为。
¥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
你可以使用 Node 功能 子路径导入 来模拟模块。子路径导入允许你为项目中的模块定义自定义路径,这些路径可用于用模拟文件替换原始模块。它们可与 Vite 和 Webpack 构建器兼容。
¥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
要模拟模块,请创建一个与要模拟的模块同名且位于同一目录中的文件。例如,要模拟名为 session 的模块,请在其旁边创建一个名为 session.mock.js|ts 的文件,并具有一些特性:
¥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.
-
它应该使用
fn实用程序来模拟原始模块中的任何必要功能。¥It should use the
fnutility to mock any necessary functionality from the original module. -
它应该使用
mockName方法来确保在最小化时保留名称¥It should use the
mockNamemethod 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.
以下是名为 session 的模块的模拟文件的示例:
¥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');当你使用 fn 实用程序模拟模块时,你将创建完整的 Vitest 模拟函数。有关如何在故事中使用模拟模块的示例,请参阅 below。
¥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
你不能直接模拟外部模块,如 uuid 或 node:fs。相反,你必须将其封装在你自己的模块中,你可以像任何其他内部模块一样模拟它。例如,使用 uuid,你可以执行以下操作:
¥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
要配置子路径导入,请在项目的 package.json 文件中定义 imports 属性。此属性将子路径映射到实际文件路径。以下示例为四个内部模块配置子路径导入:
¥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.
其次,密钥的顺序很重要。default 键应该放在最后。
¥Second, the order of the keys is important. The default key should come last.
第三,请注意每个模块条目中的 storybook、test 和 default 键。storybook 值用于在 Storybook 中加载时导入模拟文件,而 default 值用于在项目中加载时导入原始模块。test 条件也在 Storybook 中使用,它允许你在 Storybook 和其他测试中使用相同的配置。
¥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仅当 TypeScript 配置中的 moduleResolution 属性 设置为 'Bundler'、'NodeNext' 或 'Node16' 时,子路径导入才会被正确解析和输入。
¥Subpath imports will only be correctly resolved and typed when the moduleResolution property is set to 'Bundler', 'NodeNext', or 'Node16' in your TypeScript configuration.
如果你当前正在使用 'node',则该版本适用于使用 v10 之前的 Node.js 版本的项目。用现代代码编写的项目可能不需要使用 'node'。
¥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 建议使用 TSConfig 备忘单 来指导你设置 TypeScript 配置。
¥Storybook recommends the TSConfig Cheat Sheet for guidance on setting up your TypeScript configuration.
在故事中使用子路径导入
¥Using subpath imports in stories
当你使用 fn 实用程序模拟模块时,你将创建完整的 Vitest 模拟函数,其中包含许多可用的方法。一些最有用的方法包括:
¥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:
| 方法 | 描述 |
|---|---|
mockReturnValue(value) | 设置模拟函数的返回值。 |
mockResolvedValue(value) | 设置模拟异步函数解析后的值。 |
mockImplementation(fn) | 为模拟函数设置自定义实现。 |
在这里,我们在故事中定义 beforeEach(将在故事渲染之前运行),以设置页面使用的 getUserFromSession 函数的模拟返回值组件:
¥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' });
},
};如果你是 用 TypeScript 写你的故事,你必须使用完整的模拟文件名导入模拟模块,以便在故事中正确输入函数。你不需要在组件文件中执行此操作。这就是 子路径导入 或 构建器别名 的用途。
¥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
fn 实用程序还会监视原始模块的功能,你可以使用它在测试中断言它们的行为。例如,你可以使用 交互测试 来验证是否使用特定参数调用了函数。
¥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.
例如,此故事检查用户单击保存按钮时是否调用了 saveNote 函数:
¥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
如果你的项目无法使用 automocking 或 子路径导入,你可以配置 Storybook 构建器,将模块别名为 模拟文件。这将指示构建器在打包 Storybook 故事时用模拟文件替换模块。
¥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
在故事渲染之前,你可以使用异步 beforeEach 函数执行所需的任何设置(例如,配置模拟行为)。此函数可以在故事、组件(将为文件中的所有故事运行)或项目(在 .storybook/preview.js|ts 中定义,将为项目中的所有故事运行)中定义。
¥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).
你还可以从 beforeEach 返回一个清理函数,该函数将在你的故事卸载后调用。这对于取消订阅观察者等任务很有用。
¥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.
无需使用清理功能恢复 fn() 模拟,因为 Storybook 在渲染故事之前会自动执行此操作。有关更多信息,请参阅 parameters.test.restoreMocks API。
¥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.
以下是使用 mockdate 包模拟 Date 并在故事卸载时重置它的示例。
¥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
收到 exports is not defined 错误
¥Receiving an exports is not defined error
使用 automocking 时,Webpack 项目可能会遇到 exports is not defined 错误。这通常是由于尝试使用 CommonJS (CJS) 入口点模拟模块引起的。使用 Webpack 自动模拟仅适用于仅具有 ESModules (ESM) 入口点的模块,因此你必须使用 模拟文件 文件来模拟 CJS 模块。
¥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.
