
使用 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 组件编写一个 story。
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 函数中定义的步骤,与组件交互并置顶一个任务——就像用户会做的那样。如果你查看你的 interactions panel,你会看到一步步的流程。它还提供了一套方便的 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://#/fOwVOHMHQS pic.twitter.com/vcm5C4fsTL