返回博客

Storybook 中的下一代模块模拟

为您的故事提供自动、快速、强大的模拟

loading
Valentin Palkovic
最后更新

一致性是独立开发和测试 UI 的基石。您的 Storybook stories 应该每次都渲染相同的 UI,无论谁在查看它们、何时查看它们,或者您的后端服务是否在线。对您的 story 相同的输入应始终产生相同的输出。

当您组件的唯一输入是 props 时,这很简单。对于网络数据,流行的 Mock Service Worker 插件是一个绝佳的解决方案。但其他依赖项呢?您如何处理依赖于浏览器 API(如 localStorage)或需要身份验证才能工作的组件?

模拟这些输入一直是一个长期存在的挑战。在 Storybook 8.1 中,我们引入了一种基于标准的子路径导入方法。虽然功能强大,但它需要手动配置 package.json 和更改组件的导入路径。

今天,我们很高兴地推出一种下一代解决方案,它更简单、更强大,并提供卓越的开发者体验。

隆重推出 sb.mock()

新的模块模拟功能由 Vitest 经过实战检验的模拟引擎提供支持,但它已深度集成到 Storybook 的工作流程中,无论是在 **Vite 和 Webpack** 中,还是在 **开发和生产** 模式下,都能无缝工作。

这一切都围绕着一个新颖、直观的 API:sb.mock()

考虑一个用户可配置的 Dashboard 组件,它允许用户选择显示哪些信息,并将这些设置存储在浏览器的本地存储中。

// lib/settings.ts
export const getDashboardLayout = () => {
  const layout = window.localStorage.getItem('dashboard.layout');
  return layout ? JSON.parse(layout) : [];
};

// components/Dashboard.tsx
import { getDashboardLayout } from '../lib/settings.ts';

export const Dashboard = () => {
  const layout = getDashboardLayout();
  // ...logic to display layout
};

现在,您可以通过在 .storybook/preview.ts 文件中添加一行代码,全局模拟它,使其适用于所有 stories。

// .storybook/preview.ts
import { sb } from 'storybook/test';

// That's it! The settings module will now be mocked in all stories.
sb.mock('../lib/settings.ts');

这种方法更干净,无需更改您的组件代码,并且完全是类型安全的。

如何使用自动模拟

sb.mock() API 设计得非常灵活,可以直接从您的 preview 配置中满足您所有的模拟需求。

无需模拟文件的自动模拟

在大多数情况下,您可以使用自动生成的模拟。如果您调用 sb.mock('...', { spy: true }),Storybook 将用间谍替换所有导出,这些间谍在记录每次交互的同时会调用原始函数。这使您可以继续使用原始行为(或根据需要进行修改)并跟踪其使用情况。

// .storybook/preview.ts
import { sb } from 'storybook/test';

sb.mock('../lib/analytics.ts', { spy: true });

现在,在您的 stories 中,您可以从原始路径导入,并使用原始功能或为特定 story 配置模拟。这会将模拟的行为与其使用位置放在一起。

// components/Dashboard.stories.ts
import { mocked } from 'storybook/test';
import { trackEvent } from '../lib/analytics.ts';

export const MyStory = {
  beforeEach: () => {
    /*
     * The `trackEvent` function is already a mock!
     * The `mocked` utility is just for proper mock function types
     */
    mocked(trackEvent).mockResolvedValue({ status: 'ok' });
  },
  play: async () => {
    // ... interact with the component
    await expect(trackEvent).toHaveBeenCalledWith('dashboard-viewed');
  },
};

替换模块

如果您将 spy 设置为 false(或省略它),Storybook 会通过用安全、惰性值替换其导出,自动模拟模块。函数和类实例被替换为空模拟 (vi.fn()),数组被清空,对象被深度克隆,原始类型保持不变。这为您在每个 story 中提供了干净的起点。

// .storybook/preview.ts
import { sb } from 'storybook/test';

sb.mock('../lib/analytics.ts');

使用 __mocks__ 进行自动模拟

如果原始模块旁边存在一个 __mocks__ 目录中的文件,Storybook 将自动使用它。这种约定由 Jest 和 Vitest 普及,非常适合需要专用模拟实现的更复杂的模块。它还确保原始功能永远不会被执行,这在模拟浏览器环境中的服务器代码时可能很重要。

对于本地模块,请将 __mocks__ 目录放在原始模块旁边。

src/
├── lib/
│   ├── __mocks__/
│   │   └── analytics.ts  // This mock will be used automatically
│   └── analytics.ts      // The original module
└── components/
    └── Dashboard.tsx
// src/lib/__mocks__/analytics.ts
// A custom mock implementation for all stories
export const trackEvent = async () => {
  return { status: 'ok' };
};

对于 node_modules,请在您的项目根目录中创建一个 __mocks__ 目录。在其中,创建一个与您要模拟的包同名的文件。对于深度导入,请复制文件夹结构。

// project-root/__mocks__/lodash-es.js
export default {
  VERSION: 'mocked!',
};

// project-root/__mocks__/lodash-es/add.js
export default (a, b) => 'mocked add';

然后,在您的 Storybook preview 配置中注册模拟。

// .storybook/preview.ts
import { sb } from 'storybook/test';

sb.mock('../src/lib/analytics.ts');
sb.mock('lodash-es');
sb.mock('lodash-es/add');

目前,我们只支持模拟 ES Modules。不支持模拟 CommonJS 模块。

使用 import() 进行类型安全的模拟

为了改善开发者体验并防止路径错误,sb.mock 还接受动态 import() 语句。这为您提供了完整的 TypeScript 支持和 IDE 自动完成功能,确保如果您重构或移动文件,您的模拟路径将自动更新。

// .storybook/preview.ts
import { sb } from 'storybook/test';

// ✅ Type-safe, refactor-friendly, and recommended!
sb.mock(import('../lib/analytics.ts'));

在底层,Storybook 仍然基于字符串路径进行操作,但这种语法提供了更优越的创作体验。请注意,在 import() 语句中不支持任何别名路径。

工作原理:预转换

sb.mock() 背后的魔法是强大的预转换 (AOT) 策略。与我们之前操纵 package.json 的方法不同,这个新系统直接与您的打包器集成。

启动期间,自定义 Vite 和 Webpack 插件会扫描您的 .storybook/preview.ts 文件中的所有 sb.mock() 调用。然后,它们会在将应用程序的依赖图提供给浏览器或打包为生产之前,智能地重写它。

这意味着最终在浏览器中运行的代码已经包含了模拟的模块。这种方法是

  • 高性能:零运行时开销。
  • 健壮:它在开发模式和静态生产构建中运行方式相同。
  • 简单:它不需要复杂的客户端拦截或 Service Workers。

与其他模拟解决方案的比较

这个新 API 比我们以前的方法有了显著的改进,并为通用的测试工具提供了一个有针对性的替代方案。

  • 与旧的子路径导入相比:新 API 是一个巨大的飞跃。您不再需要修改 package.json、更改组件导入路径或创建样板 .mock.ts 文件。
  • 与 Vitest 相比:虽然由 Vitest 的引擎提供支持,但 Storybook 的实现针对组件的独立性进行了定制。
    • 全局范围:模拟在 preview.ts 中全局定义,并应用于所有 stories。
    • 无工厂函数:API 不接受运行时工厂(例如 sb.mock('path', () => ({}))),以确保在静态构建中的可靠性。对于复杂的模拟,请使用 __mocks__ 目录。
    • 按设计是静态的:该系统故意是静态的,以保证开发和生产构建之间的一致性。

立即尝试

模块模拟在Storybook 9.1 中可用。要开始使用,请升级您的项目。

npx storybook@latest upgrade

要了解更多信息,请参阅 Storybook 文档中的完整示例和 API 详细信息。我们迫不及待地想看到您构建的东西!

测试依赖于 localStorage 或身份验证等内容的组件可能会很棘手。Storybook 通过我们基于 @vitest.dev 出色的模拟工具构建的新模块模拟 API,让这一切变得简单。storybook.js.org/blog/next-ge...

Storybook (@storybook.js.org) 2025-08-21T18:00:06.677Z

加入 Storybook 邮件列表

获取最新消息、更新和发布信息

7,468开发者及更多

我们正在招聘!

加入 Storybook 和 Chromatic 团队。构建被数十万开发人员在生产中使用的工具。远程优先。

查看职位

热门帖子

Storybook 将仅支持 ESM

更小、更简单、更现代化
loading
Norbert de Langen

使用 Storybook 和 Vitest 进行组件测试

触手可及的一流组件测试
loading
Dominic Nguyen

Storybook 臃肿?已修复。

我们如何将 Storybook 的包大小减半
loading
Michael Shilman
加入社区
7,468开发者及更多
原因为什么选择 Storybook组件驱动的 UI
文档指南教程更新日志遥测
社区插件参与进来博客
展示探索项目组件词汇表
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI