
使用 Storybook 测试组件交互
关于如何模拟和验证用户行为的完整教程

组件获取数据、响应用户交互并管理应用状态。为了验证这种功能行为,开发者依赖于自动化测试。但是大多数测试工具都基于 Node 和 JSDOM。这意味着您被迫在*文本*命令行中调试*可视化* UI。
在 Storybook,我们正在通过使用浏览器运行测试来改进组件测试。在过去的六个月中,我们引入了几个功能——play 函数、test-runner、断言库——以实现这一目标。本文将介绍完整的 Storybook 交互测试工作流程。
- 📝 在 stories 文件中编写测试
- 🐛 使用交互面板在浏览器中调试测试
- 🔗 通过 URL 重现错误状态
- 🤖 使用持续集成自动化测试
Storybook 中的组件测试是如何工作的?
测试交互是验证用户行为的常用模式。您提供模拟数据来设置测试场景,使用 Testing Library 模拟用户交互,并检查生成的 DOM 结构。
在 Storybook 中,这种熟悉的工作流程发生在您的浏览器中。这使得调试失败更容易,因为您在与开发组件相同的环境(浏览器)中运行测试。
首先编写一个 story 来设置组件的初始状态。然后使用 play 函数模拟用户行为,例如点击和表单条目。最后,使用 Storybook test-runner 检查 UI 和组件状态是否正确更新。通过命令行或 CI 服务器自动化测试。
教程
为了演示测试工作流程,我将使用 Taskbox 应用——一个类似于 Asana 的任务管理应用。在其 InboxScreen 上,用户可以点击星标图标来置顶任务。或者点击复选框来存档它。让我们编写测试以确保 UI 正确响应这些交互。

获取代码以跟随操作
# Clone the template
npx degit chromaui/ui-testing-guide-code#dc9bacae842f5250aad544b139dc9d63a48bbd1e taskbox
cd taskbox
# Install dependencies
yarn
设置 test-runner
我们将从安装 test-runner 和相关软件包开始(注意,它需要 Storybook 6.4 或更高版本)。
yarn add -D @storybook/testing-library @storybook/jest @storybook/addon-interactions jest @storybook/test-runner
更新您的 Storybook 配置(在 `.storybook/main.js` 中)以包含 interactions 插件并启用用于调试的回放控件。
// .storybook/main.js
module.exports = {
stories: [],
addons: [
// Other Storybook addons
'@storybook/addon-interactions', // 👈 addon is registered here
],
features: {
interactionsDebugger: true, // 👈 enable playback controls
},
};
然后将测试任务添加到项目的 `package.json` 中
{
"scripts": {
"test-storybook": "test-storybook"
}
}
最后,启动您的 Storybook(test-runner 针对正在运行的 Storybook 实例运行)
yarn storybook
编写 stories 以设置测试用例
编写测试的第一步是通过向组件提供 props 或模拟数据来设置场景。这正是 story 的作用,所以让我们为 InboxScreen 组件编写一个。
InboxScreen 通过 `/tasks` API 请求获取数据,我们将使用 MSW 插件模拟它。
// src/InboxScreen.stories.js;
import React from 'react';
import { rest } from 'msw';
import { within, userEvent, findByRole } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
import { InboxScreen } from './InboxScreen';
import { Default as TaskListDefault } from './components/TaskList.stories';
export default {
component: InboxScreen,
title: 'InboxScreen',
};
const Template = (args) => <InboxScreen {...args} />;
export const Default = Template.bind({});
Default.parameters = {
msw: {
handlers: [
rest.get('/tasks', (req, res, ctx) => {
return res(ctx.json(TaskListDefault.args));
}),
],
},
};
使用 play 函数编写交互测试
Testing Library 提供了一个方便的 API 用于模拟用户交互——点击、拖动、轻触、输入等。而 Jest 提供了断言实用程序。我们将使用这两个工具的 Storybook 工具化版本来编写测试。因此,您将获得熟悉的开发者友好的语法来与 DOM 交互,但带有额外的遥测功能来帮助调试。
测试本身将包含在 play 函数中。这段代码片段附加到一个 story 并在 story 渲染后运行。
让我们添加我们的第一个交互测试,以验证用户可以置顶任务
export const PinTask = Template.bind({});
PinTask.parameters = { ...Default.parameters };
PinTask.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
const getTask = (name) => canvas.findByRole('listitem', { name });
// Find the task to pin
const itemToPin = await getTask('Export logo');
// Find the pin button
const pinButton = await findByRole(itemToPin, 'button', { name: 'pin' });
// Click the pin button
await userEvent.click(pinButton);
// Check that the pin button is now a unpin button
const unpinButton = within(itemToPin).getByRole('button', { name: 'unpin' });
await expect(unpinButton).toBeInTheDocument();
};
每个 play 函数都接收 Canvas 元素——story 的顶级容器。您可以将查询范围限定在这个元素内,从而更容易找到 DOM 节点。
在我们的例子中,我们正在寻找“Export logo”任务。然后在其中找到置顶按钮并点击它。最后,我们检查按钮是否已更新为未置顶状态。
当 Storybook 完成渲染 story 时,它会执行在 play 函数中定义的步骤,与组件交互并置顶任务——类似于用户会做的那样。如果您查看您的交互面板,您将看到逐步流程。它还提供了一组方便的 UI 控件,用于暂停、恢复、倒退和单步执行每个交互。

使用 test-runner 执行测试
现在我们已经完成了第一个测试,我们还将为存档、编辑和删除任务功能添加测试。
export const ArchiveTask = Template.bind({});
ArchiveTask.parameters = { ...Default.parameters };
ArchiveTask.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
const getTask = (name) => canvas.findByRole('listitem', { name });
const itemToArchive = await getTask('QA dropdown');
const archiveCheckbox = await findByRole(itemToArchive, 'checkbox');
await userEvent.click(archiveCheckbox);
await expect(archiveCheckbox.checked).toBe(true);
};
export const EditTask = Template.bind({});
EditTask.parameters = { ...Default.parameters };
EditTask.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
const getTask = (name) => canvas.findByRole('listitem', { name });
const itemToEdit = await getTask('Fix bug in input error state');
const taskInput = await findByRole(itemToEdit, 'textbox');
userEvent.type(taskInput, ' and disabled state');
await expect(taskInput.value).toBe(
'Fix bug in input error state and disabled state'
);
};
export const DeleteTask = Template.bind({});
DeleteTask.parameters = { ...Default.parameters };
DeleteTask.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
const getTask = (name) => canvas.findByRole('listitem', { name });
const itemToDelete = await getTask('Build a date picker');
const deleteButton = await findByRole(itemToDelete, 'button', {
name: 'delete',
});
await userEvent.click(deleteButton);
expect(canvas.getAllByRole('listitem').length).toBe(5);
};
您现在应该看到这些场景的 stories。Storybook 仅在您查看 story 时运行交互测试。因此,您必须遍历每个 story 才能运行所有检查。
每次进行更改时手动审查整个 Storybook 是不现实的。Storybook test-runner 自动化了这个过程。它是一个独立的实用程序——由 Playwright 驱动——它运行所有交互测试并捕获损坏的 stories。

启动 test-runner(在单独的终端窗口中):`yarn test-storybook --watch`。它验证所有 stories 是否在没有任何错误的情况下渲染,以及是否所有断言都已通过。

如果测试失败,您将获得一个链接,该链接会在浏览器中打开失败的 story。

您已经整理好了本地开发工作流程。Storybook 和 test-runner 并排运行,使您能够隔离构建组件并一次性测试其底层逻辑。
自动化 Storybook 交互测试
一旦您准备好合并代码,您将希望使用持续集成 (CI) 服务器自动运行所有检查。您有两种将 Storybook 交互测试集成到测试自动化管道中的选项:在 CI 中使用 test-runner 或将其与使用 Chromatic 的可视化测试相结合。
在 CI 中运行 test-runner
您可以在您的 CI 服务器上构建和提供 Storybook,并针对它执行 test-runner。这是一个使用 concurrently、http-server 和 wait-on 库的配方。
# .github/workflows/ui-tests.yml
name: 'Storybook Tests'
on: push
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14.x'
- name: Install dependencies
run: yarn
- name: Install Playwright
run: npx playwright install --with-deps
- name: Build Storybook
run: yarn build-storybook --quiet
- name: Serve Storybook and run tests
run: |
npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
"npx http-server storybook-static --port 6006 --silent" \
"npx wait-on tcp:6006 && yarn test-storybook"
您还可以针对已发布的 Storybook 运行测试。有关更多信息和其它 CI 配置选项,请参阅 test-runner 文档。
使用 Chromatic 结合交互和可视化测试
捕获意外的 UI 更改一直是一个挑战。一行有漏洞的 CSS 可能会破坏多个页面。这就是为什么 Auth0、Twilio、Adobe 和 Peloton 的领先团队依赖于可视化测试。Chromatic 是一个云端可视化测试工具,专为 Storybook 构建。它还可以执行您的交互测试。
Chromatic 的工作原理是捕获每个 story 的图像快照——就像它在浏览器中显示的那样。然后,当您打开拉取请求时,它会将其与之前接受的基线进行比较,并向您展示差异。


Chromatic 开箱即用地支持 Storybook 交互测试。它会等待交互测试运行完毕后再捕获快照。这样,您就可以一次性验证组件的视觉外观和底层逻辑。任何测试失败都会通过 Chromatic UI 报告。
这是一个使用 Github Actions 运行 Chromatic 的示例工作流程。对于其它 CI 服务,请参阅 Chromatic 文档。
# .github/workflows/ui-tests.yml
name: 'Chromatic'
on: push
jobs:
chromatic-deployment:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Required to retrieve git history
- name: Install dependencies
run: yarn
- name: Publish to Chromatic
uses: chromaui/action@v1
with:
# Grab this from the Chromatic manage page
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
使用 Storybook 在浏览器中测试组件
组件不是静态的。用户可以与 UI 交互并触发状态更新。您必须编写模拟用户行为的测试来验证这种行为。
Storybook 交互测试是我们对组件测试应有的愿景:快速、直观,并与您已经使用的工具集成。它将实时浏览器的直观调试环境与无头浏览器的性能和可脚本性相结合。
如果您一直在跟随编码,您的存储库应该看起来像这样:GitHub 存储库。
想要了解更多?这里有一些额外的有用资源
- Storybook 交互测试文档
- Test runner 文档
- 交互面板文档
- UI 测试手册,深入了解 UI 测试
组件管理状态并响应用户交互。
— Storybook (@storybookjs) 2022 年 4 月 6 日
您可以使用 Storybook 测试此行为
📝 将初始状态设置为 story
🐙 使用 play 函数模拟事件
✅ 使用 test-runner 断言 DOM 结构
开始使用我们的新教程:https://127.0.0.1/fOwVOHMHQS?ref=storybookblog.ghost.io pic.twitter.com/vcm5C4fsTL