ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Storybook Automocking 系列教程:在 .storybook/preview.* 中用 `sb.mock` 注册 Mock 文件(register mock file)

Storybook Automocking 系列教程:在 .storybook/preview.* 中用 `sb.mock` 注册 Mock 文件(register mock file) Storybook Automocking 系列教程在 .storybook/preview.* 中用sb.mock注册 Mock 文件register mock file本篇技术指南聚焦 Storybook 自动模拟Automocking三种注册方式中的Mock 文件Mock files模式如何在项目级配置.storybook/preview.*中通过storybook/test的sb.mock工具注册本地模块与node_modules包的自定义 mock 文件从而实现复杂、可复用、可跨 story 共享的模块替换。读完本文将掌握 mock 文件目录布局、sb.mock各语法形态CSF 3 与 CSF Next、TS 与 JS、路径解析规则以及底层源码的运行原理。背景Automocking 的两种能力来源Storybook 在storybook/test中提供sb.mock()工具注册需要模拟的模块。同一套 API 支持两种替换来源自动生成 mock/spy当未找到对应 mock 文件时Storybook 在构建期自动把原模块的导出替换为 Vitest mock 函数Mock 文件本文主题开发者预先编写一个真实的替换文件通常放在__mocks__目录sb.mock会优先查找并使用该文件。正如 mocking-modules.mdx 中的说明注册自动 mock 与注册 mock 文件的 API 完全一致唯一区别是sb.mock会先在对应目录查找是否存在 mock 文件redirect找不到才回退到自动 mock。为什么需要 mock 文件当模拟逻辑较复杂、或需要在多个 story 间复用一个 mock 行为时把模拟实现写进独立文件更清晰同时 mock 文件能彻底阻断原模块代码执行——这一点与完全自动 mock不同后者虽然会替换导出函数但模块自身及其依赖仍会被求值相关说明。第一步按规则创建 Mock 文件本地模块的 mock 文件针对项目内的本地模块在与模块同级的__mocks__目录下创建同名文件。例如要模拟lib目录下的session模块lib/ ├── session.ts └── __mocks__/ └── session.tsexport function getUserFromSession() { return { name: Mocked User }; }node_modules 包的 mock 文件针对外部依赖包在项目根目录的__mocks__目录创建 mock 文件。例如模拟uuid包__mocks__/ └── uuid.jsexport function v4() { return 1234-5678-90ab-cdef; }若外部模块带深层导入路径如lodash-es/add需要按路径层级建目录例如__mocks__/lodash-es/add.js。项目根目录随构建器不同而不同构建器根__mocks__目录位置ViteVite 配置中的root目录通常为process.cwd()若无法解析则回退到包含.storybook的目录WebpackWebpack 配置中的context目录通常为process.cwd()若无法解析则回退到仓库根目录Mock 文件硬性要求必须用JavaScript编写不能用 TypeScript且使用ESModules不能用 CJS必须与原模块导出同名的命名导出named exports如需模拟默认导出可在 mock 文件中使用export default。第二步在.storybook/preview.*注册 Mock 文件注册 mock 文件的调用放在项目级配置.storybook/preview.tsx或.jsx/.ts/.js中这正是本文目标代码片段 automock-register-mock-file.md 的核心内容。它同时替换两处依赖本地模块../lib/session.ts与外部包uuid。CSF 3 形态TypeScript// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, vue3-vite, sveltekit) import type { Preview } from storybook/your-framework; 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)); const preview: Preview { // ... }; export default preview;CSF 3 形态JavaScriptimport { sb } from storybook/test; // Replaces imports of this module with imports to ../lib/__mocks__/session.ts sb.mock(../lib/session.js); // Replaces imports of this module with imports to ../__mocks__/uuid.ts sb.mock(uuid); export default { // ... };CSF Next 形态definePreview使用新实验性 CSF Next 语法时改为从框架包导入definePreviewsb.mock调用体不变。以 React 与 Vue 为例// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite) import { definePreview } from storybook/your-framework; 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)); export default definePreview({ // ... });import { definePreview } from storybook/vue3-vite; import { sb } from storybook/test; sb.mock(import(../lib/session.ts)); sb.mock(import(uuid)); export default definePreview({ // ... });CSF Next 形态下definePreview的导入源随渲染器切换代码骨架完全相同渲染器definePreview 导入源Reactstorybook/your-frameworkreact-vite、nextjs、nextjs-vite 等Vuestorybook/vue3-viteAngularstorybook/angularWeb Componentsstorybook/web-components-vite两行注释意味着什么上面示例里的两行注释揭示了一条关键事实mock 文件路径并非由开发者显式写出。sb.mock(import(../lib/session.ts))指向的是待模拟的原模块系统会自动到其相邻的__mocks__目录查找替换文件sb.mock(import(uuid))则会到项目根__mocks__下查找uuid.js。这就是文档所称API 相同、仅查找顺序不同的实现形态。sb.mock注册的核心约束务必遵守围绕注册 mock 文件与自动 mocksb.mock的规则是相同的mocking-modules.mdx注册位置只能在项目级配置.storybook/preview.*中注册。story 文件中不能调用sb.mock注册新模块只能通过beforeEach/play等运行时修改已注册 mock 的行为可注册对象本地模块如../lib/session.ts与node_modules包如uuid均可本地模块的路径要求不得使用别名或 subpath如/lib/session.ts、#lib/session必须相对于.storybook/preview.*文件本身必须包含文件扩展名如.ts或.jsTypeScript 推荐写法用import()包裹模块路径如sb.mock(import(../lib/session.ts))以保证模块能被正确解析与获得类型Webpack 用户的额外限制Webpack 构建器只能自动 mock 拥有纯 ESM 入口的node_modules包。若包同时提供 CJS 与 ESM 入口Webpack 无法正确解析 ESM 入口此时应改用 mock 文件方式automock-register-mock-file的场景正是解决此问题的推荐路径。若强行走自动 mock常见报错为exports is not defined。在 Story 中控制 Mock 文件的行为并断言mock 文件导出的函数会被注册为完整的 Vitest mock 函数因此可在 story 的beforeEach在渲染前执行或play中设置返回值并断言调用。注意此时应使用storybook/test的mocked工具获取正确的 TS 类型它是vi.mocked的类型安全封装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, // Runs before each story renders beforeEach: async () { // Force known, consistent behavior for mocked modules mocked(uuidv4).mockReturnValue(1234-5678-90ab-cdef); mocked(getUserFromSession).mockReturnValue({ name: John Doe }); }, } satisfies Metatypeof AuthButton; export default meta; type Story StoryObjtypeof 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(); }, };由于 mock 文件提供的是完整 Vitest mock 函数最常用的方法包括方法用途mockReturnValue(value)设定同步返回值mockResolvedValue(value)设定异步函数 resolve 的值mockImplementation(fn)设定自定义实现无需手工清理这些 mockStorybook 在渲染每条 story 前会自动恢复 mock对应parameters.test.restoreMocks行为。源码级原理sb.mock如何找到并注入 Mock 文件从本仓库源码可以进一步印证注册机制的两段式处理。阶段一解析与重定向resolve在 code/core/src/mocking-utils/resolve.ts 中resolveMock()resolve.ts#L54-L79先判断模块是否为外部包外部包通过resolveExternalModule()解析——使用oxc-resolver条件为browser/import/module/default优先命中exports映射与package.json的browser字段resolve.ts#L15-L36本地模块则用require.resolve(path, { paths: [dirname(importer)] })以preview 文件所在目录为基准解析印证了相对.storybook/preview.*的约束随后调用findMockRedirect在对应__mocks__目录查找替换文件得到redirectPath找不到时为null从而回退到自动 mock。阶段二构建期代码变换automock / autospy当没有 mock 文件时构建器会改写原模块导出。在 code/core/src/mocking-utils/automock.ts 中getAutomockCode()automock.ts#L17-L22调用基于 MagicString 的automockModule()解析原模块 AST收集所有命名导出函数、变量、类、重导出与默认导出通过globalThis[__vitest_mocker__]访问 Vitest mock 注册表调用mockObject(module, automock | autospy)生成替换后的模块对象autospy对应{ spy: true }保留原行为automock对应默认的完全替换生成的新模块再以export { ... }重新声明。因此所有替换决策在构建期静态完成产物直接内联真正的 mock 模块无运行时拦截开销。这套变换逻辑被 vite-mock 插件扫描.storybook/preview.*中的sb.mock()调用与 Webpack 侧 webpack-automock-loader 复用。开发模式下mock 文件的新增/变更还会通过 Vite 模块图失效机制触发热更新。与 Vitest mocking 的差异仓库文档明确强调其与 Vitest 原生 mocking 的差异mocking-modules.mdxmock 全局化且仅限.storybook/preview.*决策静态化于构建期因此没有sb.unmock()也不接受工厂函数sb.mock(path, () ({...}))——工厂函数是运行时行为与构建期静态替换矛盾。mock 文件的函数行为仍可在 story 的play/beforeEach中运行时修改。仓库内的自动化验证本仓库自带的示例与测试可以佐证上述全部机制ModuleAutoMocking.stories.ts 与其mocks目录下的工具 mock演示本地模块注册 mock 文件后的 story 编写方式NodeModuleMocking.stories.js 演示node_modules包级 mock端到端层面 sb-module-mocking.spec.ts 覆盖了模块模拟的完整流程。小结Mock 文件是 Storybook Automocking 中最灵活的一种注册形态本地模块把替换文件放在模块相邻的__mocks__目录外部包把替换文件放在项目根__mocks__目录随后在.storybook/preview.*用一行sb.mock(import(../lib/session.ts))或sb.mock(import(uuid))完成全局注册mock 文件导出即成为可断言、可复用的 Vitest mock 函数。理解先查 mock 文件、后自动 mock的分流逻辑与构建期静态替换的机制能帮助你在 Vite/Webpack 项目中稳定地隔离组件的外部依赖。创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进