组件也可以依赖于导入到组件文件的模块。这些模块可以来自外部包,也可以来自项目内部。在 Storybook 中渲染这些组件或测试它们时,您可能希望模拟这些模块以控制它们的运行方式。
ℹ️如果您喜欢通过示例学习,我们创建了一个 完整的演示项目,该项目使用此处描述的模拟策略。
在 Storybook 中模拟模块主要有两种方法。这两种方法都涉及创建模拟文件以替换原始模块。这两种方法之间的区别在于您如何将模拟文件导入到您的组件中。
对于这两种方法,都不支持模拟模块的相对导入。
要模拟模块,请创建一个与您要模拟的模块同名且位于同一目录下的文件。例如,要模拟一个名为 session
的模块,请在它旁边创建一个名为 session.mock.js|ts
的文件,该文件具有以下几个特点
- 它必须使用相对导入导入原始模块。
- 它应该重新导出原始模块中的所有导出。
- 它应该使用
fn
工具模拟原始模块中的任何必要功能。
- 它应该使用
mockName
方法确保在缩小代码时名称保留。
- 它不应该引入可能影响其他测试或组件的副作用。模拟文件应该是隔离的,只影响它们模拟的模块。
以下是一个名为 session
的模块的模拟文件示例
当您使用 fn
工具模拟模块时,您会创建完整的 Vitest 模拟函数。请参阅 以下内容,了解如何在故事中使用模拟模块的示例。
您无法直接模拟像 uuid
或 node:fs
这样的外部模块。相反,您必须将其包装在您自己的模块中,然后像任何其他内部模块一样模拟它。例如,对于 uuid
,您可以执行以下操作
并为包装器创建一个模拟
模拟模块的推荐方法是使用 子路径导入,这是 Node 包的一个功能,由 Vite 和 Webpack 都支持。
要配置子路径导入,您需要在项目的 package.json
文件中定义 imports
属性。此属性将子路径映射到实际的文件路径。以下示例配置了四个内部模块的子路径导入
此配置有三个方面值得注意
首先,每个子路径都必须以 #
开头,以将其与常规模块路径区分开来。#*
条目是一个通配符,它将所有子路径映射到根目录。
其次,键的顺序很重要。 default
键应该放在最后。
第三,请注意每个模块条目中的 storybook
、test
和 default
键。storybook
值用于在 Storybook 中加载时导入模拟文件,而 default
值用于在项目中加载时导入原始模块。test
条件也用于 Storybook 中,这使得您可以在 Storybook 和其他测试中使用相同的配置。
配置好包后,您可以更新组件文件以使用子路径导入
ℹ️只有当 moduleResolution
属性 在您的 TypeScript 配置中设置为 'Bundler'
、'NodeNext'
或 'Node16'
时,子路径导入才会被正确解析并类型化。
如果您目前使用的是 'node'
,则它是针对使用低于 v10 版本的 Node.js 的项目。使用现代代码编写的项目可能不需要使用 'node'
。
Storybook 建议使用 TSConfig Cheat Sheet 来指导您设置 TypeScript 配置。
如果您的项目无法使用子路径导入,您可以配置您的 Storybook 构建器将模块别名到模拟文件。这将指示构建器在捆绑 Storybook 故事时用模拟文件替换模块。
当您使用fn
实用程序来模拟模块时,您创建了完整的Vitest 模拟函数,它们具有许多有用的方法。例如,您可以使用mockReturnValue
方法为模拟函数设置返回值,或使用mockImplementation
来定义自定义实现。
在这里,我们在故事上定义了beforeEach
(将在故事渲染之前运行),以设置getUserFromSession
函数的模拟返回值,该函数由 Page 组件使用。
fn
实用程序还会监视原始模块的函数,您可以使用它来断言测试中的行为。例如,您可以使用组件测试来验证某个函数是否使用特定参数被调用。
例如,这个故事检查当用户单击保存按钮时,是否调用了saveNote
函数。
在故事渲染之前,您可以使用异步beforeEach
函数来执行所需的任何设置(例如,配置模拟行为)。此函数可以在故事、组件(将为文件中的所有故事运行)或项目(在.storybook/preview.js|ts
中定义,将为项目中的所有故事运行)中定义。
您还可以从beforeEach
中返回一个清理函数,该函数将在您的故事卸载后被调用。这对于诸如取消订阅观察者等任务很有用。
以下是如何使用mockdate
包来模拟Date
并在故事卸载时重置它的示例。