在构建更复杂的 UI(如页面)时,组件不仅仅负责渲染 UI,还会获取数据和管理状态。组件测试允许你验证这些 UI 的功能方面。
简而言之,你首先为组件的初始状态提供适当的 props。然后模拟用户行为,例如点击和表单输入。最后,检查 UI 和组件状态是否更新正确。
在 Storybook 中,这个熟悉的流程在你的浏览器中发生。这使得调试错误更容易,因为你在与开发组件相同的环境(浏览器)中运行测试。
你首先编写一个 故事 来设置组件的初始状态。然后使用 play 函数模拟用户行为。最后,使用 测试运行器 确认组件是否渲染正确,以及你的组件测试是否通过 play 函数。测试运行器可以通过命令行或 CI 运行。
要使用 Storybook 启用完整的组件测试体验,你需要采取额外的步骤来正确设置它。我们建议你在继续进行其他所需配置之前,先阅读 测试运行器文档。
运行以下命令安装交互插件和相关依赖项。
更新你的 Storybook 配置文件(在 .storybook/main.js|ts
中)以包含交互插件。
测试本身是在一个 play
函数中定义的,该函数连接到一个故事。以下是如何使用 Storybook 和 play
函数设置组件测试的示例
故事加载到 UI 后,它会模拟用户的行为并验证底层逻辑。
你可以在 play
方法中使用 mount
函数在渲染之前执行代码。
以下是如何使用 mockdate
包来模拟 Date
对象,这是一种使你的故事以一致状态呈现的有用方法。
⚠️使用 mount
函数有两个要求。
- 你必须从
context
(传递给你的 play 函数的参数)中解构 mount 属性。这可以确保 Storybook 在 play 函数开始之前不会开始渲染故事。
- 你的 Storybook 框架或构建器必须配置为转译成 ES2017 或更新的版本。这是因为解构语句和 async/await 的使用会被转译掉,这会阻止 Storybook 识别你对
mount
的使用。
你还可以使用 mount
来创建想要传递给组件的模拟数据。为此,首先在 play 函数中创建你的数据,然后使用配置了该数据的组件调用 mount
函数。在本例中,我们创建了一个模拟的 note
并将其 id
传递给 Page 组件,然后使用 mount
调用它。
ℹ️当你不带任何参数调用 mount()
时,组件将使用故事的渲染函数进行渲染,无论使用 隐式默认 还是 显式自定义定义。
当你在 mount
函数中像上面示例那样安装特定组件时,故事的渲染函数将被忽略。这就是为什么你必须将 args
转发给组件的原因。
有时你可能需要在文件中的每个故事之前运行相同的代码。例如,你可能需要设置组件或模块的初始状态。你可以通过在组件元数据中添加一个异步的 beforeEach
函数来实现。
你可以从 beforeEach
函数返回一个清理函数,该函数将在每个故事之后运行,当故事重新安装或从该故事导航时。
当你 更改组件的状态 时,在渲染另一个故事之前重置该状态非常重要,以保持测试之间的隔离。
有两种重置状态的方法,beforeAll
和 beforeEach
。
预览文件(.storybook/preview.js|ts
)中的 beforeAll
函数将在项目中的任何故事之前运行一次,并且不会在故事之间重新运行。除了在启动测试运行时进行初始运行之外,它不会再次运行,除非预览文件被更新。这是一个引导项目或运行你的整个项目依赖的任何设置的合适地方,如以下示例所示。
你可以从 beforeAll
函数返回一个清理函数,该函数将在重新运行 beforeAll
函数之前或在测试运行程序的拆卸过程中运行。
与只运行一次的 beforeAll
不同,预览文件(.storybook/preview.js|ts
)中的 beforeEach
函数将在项目中的每个故事之前运行。这最适合用于重置所有或大多数故事使用的状态或模块。在下面的示例中,我们使用它来重置模拟的 Date。
你可以从 beforeEach
函数返回一个清理函数,该函数将在每个故事之后运行,当故事重新安装或从该故事导航时。
在幕后,Storybook 的 @storybook/test
包提供了 Testing Library 的 user-events
API。如果你熟悉 Testing Library,那么你应该在 Storybook 中得心应手。
以下是以简化的形式呈现用户事件 API。有关更多内容,请查看 官方用户事件文档。
用户事件 | 描述 |
---|
clear | 选择输入框或文本区域内的文本并将其删除。
userEvent.clear(await within(canvasElement).getByRole('myinput')); |
click | 单击元素,调用 click() 函数。
userEvent.click(await within(canvasElement).getByText('mycheckbox')); |
dblClick | 双击元素。
userEvent.dblClick(await within(canvasElement).getByText('mycheckbox')); |
deselectOptions | 取消选中选择元素的特定选项。
userEvent.deselectOptions(await within(canvasElement).getByRole('listbox'),'1'); |
hover | 将鼠标悬停在元素上。
userEvent.hover(await within(canvasElement).getByTestId('example-test')); |
keyboard | 模拟键盘事件。
userEvent.keyboard(‘foo’); |
selectOptions | 选中选择元素的指定选项或选项。
userEvent.selectOptions(await within(canvasElement).getByRole('listbox'),['1','2']); |
type | 在输入框或文本区域中写入文本。
userEvent.type(await within(canvasElement).getByRole('my-input'),'Some text'); |
unhover | 取消鼠标悬停在元素上。
userEvent.unhover(await within(canvasElement).getByLabelText(/Example/i)); |
Storybook 的 @storybook/test
还提供了来自 Vitest 的 API,例如 expect
和 vi.fn
。这些 API 改善了你的测试体验,帮助你断言函数是否被调用、元素是否存在于 DOM 中,以及更多其他内容。如果你习惯于使用来自 Jest 或 Vitest 等测试包的 expect
,那么你就可以用几乎相同的方式编写组件测试。
对于复杂的流程,使用 step
函数将相关的交互集合分组在一起可能会有所帮助。这允许你提供一个自定义标签来描述一组交互。
这将以可折叠组的形式显示你的交互。
如果您的组件依赖于导入到组件文件中的模块,则可以模拟这些模块以控制和断言其行为。 这在模拟模块指南中详细介绍。
然后,您可以将模拟模块(具有 Vitest 模拟函数 的所有有用方法)导入到您的故事中,并使用它来断言您的组件的行为
如果您检查您的交互面板,您将看到逐步流程。 它还提供了一组方便的 UI 控件来暂停、恢复、倒带和逐步执行每次交互。
在渲染故事后执行 play
函数。 如果出现错误,它将显示在交互添加面板中以帮助调试。
由于 Storybook 是一个 Web 应用程序,任何拥有 URL 的人都可以使用相同的详细信息来重现错误,而无需任何额外的环境配置或工具。
通过在拉取请求中自动 发布 Storybook 来进一步简化组件测试。 这为团队提供了一个通用参考点来测试和调试故事。
Storybook 仅在您查看故事时运行组件测试。 因此,您必须浏览每个故事才能运行所有检查。 随着您的 Storybook 的增长,手动检查每次更改变得不切实际。 Storybook 测试运行器 通过自动为您运行所有测试来实现自动化。 要执行测试运行器,请打开一个新的终端窗口并运行以下命令
💡如果需要,您可以向测试运行器提供其他标志。 阅读文档以了解更多信息。
一旦您准备好将代码推送到拉取请求中,您将希望在合并之前使用持续集成 (CI) 服务自动运行所有检查。 阅读我们的文档以获取有关设置 CI 环境以运行测试的详细指南。
当对每个组件进行整体应用时,组件测试的维护成本可能很高。 我们建议将它们与其他方法(如视觉测试)结合使用,以更少的维护工作获得全面的覆盖范围。
组件测试将 Jest 和 Testing Library 集成到 Storybook 中。 最大的好处是能够在真实浏览器中查看您正在测试的组件。 这有助于您进行视觉调试,而不是在命令行中获得(虚假)DOM 的转储或遇到 JSDOM 模拟浏览器功能的方式的限制。 将故事和测试保存在同一个文件中比将它们分散到不同的文件中更方便。
了解其他 UI 测试