组件获取数据,响应用户的互动,并管理应用程序的状态。为了验证这种功能行为,开发人员依靠自动测试。但大多数测试工具是基于Node和JSDOM的。这意味着你不得不在文本命令行中调试可视化的UI。
在Storybook,我们正在通过使用浏览器运行测试来改进组件测试。在过去的六个月里,我们引入了几个功能--播放功能、测试运行器、断言库--使其成为现实。这篇文章介绍了整个Storybook交互测试的工作流程:
- 📝 在故事文件中编写测试
- 🐛 在浏览器中使用交互面板调试测试
- 🔗 通过URL重现错误状态
- 🤖 使用持续集成实现测试自动化
Storybook中的组件测试是如何进行的?
测试交互是验证用户行为的一种广泛的模式。你提供模拟数据来设置一个测试场景,使用测试库模拟用户交互,并检查结果的DOM结构。
在Storybook中,这个熟悉的工作流程发生在你的浏览器中。这使得调试故障更加容易,因为你在开发组件的同一环境中运行测试,即浏览器。
首先,写一个故事来设置组件的初始状态。然后使用play函数模拟用户行为,如点击和表单输入。最后,使用Storybook测试运行器来检查UI和组件的状态是否正确更新。通过命令行或你的CI服务器进行自动测试。
教程
为了演示测试工作流程,我将使用Taskbox应用程序--类似于Asana的任务管理应用程序。在它的收件箱屏幕上,用户可以点击星星图标来钉住一个任务。或者点击复选框将其存档。让我们编写测试,以确保用户界面对这些交互的响应是正确的:

拿起代码来跟着做吧:
# Clone the template
npx degit chromaui/ui-testing-guide-code#dc9bacae842f5250aad544b139dc9d63a48bbd1e taskbox
cd taskbox
# Install dependencies
yarn
设置测试运行器
我们将首先安装测试运行器和相关的软件包(注意,它需要Storybook 6.4或以上版本):
yarn add -D @storybook/testing-library @storybook/jest @storybook/addon-interactions jest @storybook/test-runner
更新你的Storybook配置(在.storybook/main.js ),以包括互动插件和启用调试的回放控制:
// .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(测试运行器针对运行中的Storybook实例运行):
yarn storybook
编写故事来设置测试案例
编写测试的第一步是通过向一个组件提供道具或模拟数据来设置一个场景。这正是一个故事,所以让我们为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函数写一个交互测试
测试库提供了一个方便的API来模拟用户的交互,如点击、拖动、敲击、输入等。而Jest则提供了断言工具。我们将使用这两个工具的Storybook-instrumented版本来编写测试。因此,你会得到一个熟悉的开发者友好的语法来与DOM交互,但有额外的遥测技术来帮助调试。
测试本身将被放置在一个播放函数中。这段代码被附加到一个故事上,并在故事被渲染后运行。
让我们加入我们的第一个交互测试,以验证用户可以钉住一个任务:
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();
};
每个播放函数都接收Canvas元素--故事的顶级容器。你可以在这个元素的范围内进行查询,这样可以更容易找到DOM节点。
在我们的例子中,我们要找的是 "导出标识 "任务。然后找到其中的pin按钮并点击它。最后,我们检查该按钮是否已经更新为未钉住的状态。
当Storybook完成了故事的渲染,它就会执行在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);
};
现在你应该看到这些场景的故事。Storybook只在你查看一个故事时运行交互测试。因此,你必须通过每个故事来运行所有的检查。
每当你做一个改变时,手动审查整个Storybook是不现实的。Storybook test-runner将这个过程自动化。它是一个独立的工具,由Playwright驱动,运行你所有的交互测试,并捕捉到损坏的故事:

启动测试运行器(在一个单独的终端窗口)。yarn test-storybook --watch.它验证所有故事的渲染是否没有任何错误,所有断言是否通过。

如果测试失败,你会得到一个链接,在浏览器中打开失败的故事:

你已经得到了本地开发工作流程的排序。Storybook和测试运行器并排运行,使你能够孤立地构建组件并一次性地测试它们的底层逻辑。
自动进行Storybook的交互测试
一旦你准备好合并你的代码,你会希望使用持续集成(CI)服务器来自动运行所有的检查。你有两个选择,可以将Storybook交互测试集成到你的测试自动化管道中:通过使用CI中的测试运行器或使用Chromatic将其与可视化测试相结合。
在CI中运行测试运行器
你可以在你的CI服务器上构建和提供Storybook,并针对它执行测试运行器。这里有一个配方,使用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的工作原理是捕捉每个故事的图像快照--就像它在浏览器中显示的那样。然后,当你打开一个拉动请求时,它会将其与之前接受的基线进行比较,并向你展示一个差异:


Chromatic支持开箱即用的Storybook交互测试。它在捕捉快照之前会等待交互测试的运行。这样,你可以一次性验证一个组件的视觉外观和底层逻辑。任何测试失败都会通过Chromatic用户界面报告。
下面是一个使用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交互测试是我们对组件测试应有的愿景:快速、直观,并与你已经使用的工具集成。它结合了实时浏览器的直观调试环境和无头浏览器的性能和脚本性。