
复合组件测试
防止小改动演变成重大回归

特斯拉刚刚召回了 158,000 辆汽车,因为一个模块(显示屏)发生故障。如果显示控制台损坏,您将无法访问后视摄像头、转向灯或驾驶员辅助系统。这会显著增加碰撞风险。
一个有缺陷的模块升级为重大故障。
UI 也面临着类似的挑战,因为应用程序就像汽车一样,是相互连接的部件网络。一个组件中的错误会影响其周围的所有其他组件。更不用说应用程序中每个使用它的部分。测试 UI 组件的组合方式有助于您防止此类错误。
在上一篇文章中,我们学习了如何使用 Storybook 隔离构建组件,编写可视化测试,并使用 Chromatic 自动捕获回归。但是,测试 UI 中更复杂的部分是很棘手的。它们是通过组合许多更简单的组件创建的,并且也连接到应用程序状态。
本文教您如何隔离复合组件并对其应用可视化测试。在此过程中,您将了解模拟数据和模拟应用程序逻辑的方法。以及测试组件集成的方法。
小错误最终会破坏应用程序
应用程序是通过将组件相互插入来构建的。这意味着一个元素中的错误可能会影响其相邻元素。例如,重命名一个 prop 可能会中断从父组件到子组件的数据流。或者 UI 元素中不正确的 CSS 通常会导致布局损坏。


以 Storybook 设计系统中的 Button 组件为例。它在多个页面中被无数次使用。Button 中的错误将无意中导致所有这些页面中的错误。换句话说,一个故障可能会成倍增加。当您在组件层次结构中向上移动到页面级别时,这些错误的影响会增加。因此,我们需要一种方法来尽早发现此类级联问题并找出根本原因。

组合测试
可视化测试通过捕获和比较故事的图像快照(在真实的浏览器中)来捕获错误。这使得它们非常适合发现 UI 更改并识别根本原因。以下是对该过程的快速回顾
- 🏷 隔离组件。使用 Storybook 一次测试一个组件。
- ✍🏽 写出测试用例。每个组件状态都使用 props 再现。
- 🔍 手动验证每个测试用例的外观。
- 📸 使用视觉回归测试自动捕获错误。
组合测试是指对树中较高层级的“复合”组件(由几个更简单的组件组成)运行可视化测试。这样,您就可以量化任何更改可能对整个应用程序产生的影响。并确保系统作为一个整体正常工作。
关键的区别在于,复合组件跟踪应用程序状态并将行为向下传递到树中。在编写测试用例时,您必须考虑到这些。
让我们看看这个过程是如何运作的。我们将使用我在第 2 部分中介绍的 Taskbox 应用程序。获取代码并跟随操作。我们的起点是 visual-testing
分支。
教程
TaskList
显示属于用户的完整任务列表。它将固定的任务移动到列表顶部。并具有加载和空状态。我们将首先为所有这些场景编写故事。

创建一个故事文件,注册 TaskList
组件并为默认情况添加一个故事。
// TaskList.stories.js
import React from 'react';
import { TaskList } from './TaskList';
import Task from './Task.stories';
export default {
component: TaskList,
title: 'TaskList',
argTypes: {
...Task.argTypes,
},
};
const Template = (args) => <TaskList {...args} />;
export const Default = Template.bind({});
Default.args = {
tasks: [
{ id: '1', state: 'TASK_INBOX', title: 'Build a date picker' },
{ id: '2', state: 'TASK_INBOX', title: 'QA dropdown' },
{
id: '3',
state: 'TASK_INBOX',
title: 'Write a schema for account avatar component',
},
{ id: '4', state: 'TASK_INBOX', title: 'Export logo' },
{ id: '5', state: 'TASK_INBOX', title: 'Fix bug in input error state' },
{ id: '6', state: 'TASK_INBOX', title: 'Draft monthly blog to customers' },
],
};
请注意 argTypes
。Args 是 Storybook 用于定义故事输入的机制。可以将它们视为与框架无关的 props。在组件级别定义的 Args 会自动传递到每个故事。在我们的例子中,我们使用 Actions 插件定义了三个事件处理程序。
当您与 TaskList
交互时,这些模拟的操作将显示在插件面板中。允许您验证组件是否正确连接。

组合 args
就像您组合组件来创建新的 UI 一样,您可以组合 args 来创建新的故事。复合组件的 args 通常甚至会组合来自其子组件的 args。
事件处理程序 args 已经在 Task 故事文件中定义,我们可以重复使用它们。同样,我们也可以使用默认故事中的 args 来创建固定的任务故事。
// TaskList.stories.js
import React from 'react';
import { TaskList } from './TaskList';
import Task from './Task.stories';
export default {
component: TaskList,
title: 'TaskList',
argTypes: {
...Task.argTypes,
},
};
const Template = (args) => <TaskList {...args} />;
export const Default = Template.bind({});
Default.args = {
tasks: [
{ id: '1', state: 'TASK_INBOX', title: 'Build a date picker' },
{ id: '2', state: 'TASK_INBOX', title: 'QA dropdown' },
{
id: '3',
state: 'TASK_INBOX',
title: 'Write a schema for account avatar component',
},
{ id: '4', state: 'TASK_INBOX', title: 'Export logo' },
{ id: '5', state: 'TASK_INBOX', title: 'Fix bug in input error state' },
{ id: '6', state: 'TASK_INBOX', title: 'Draft monthly blog to customers' },
],
};
export const WithPinnedTasks = Template.bind({});
WithPinnedTasks.args = {
tasks: [
{ id: '6', title: 'Draft monthly blog to customers', state: 'TASK_PINNED' },
...Default.args.tasks.slice(0, 5),
],
};
export const Loading = Template.bind({});
Loading.args = {
tasks: [],
loading: true,
};
export const Empty = Template.bind({});
Empty.args = {
...Loading.args,
loading: false,
};
通过 args 组合塑造故事是一种强大的技术。它允许我们编写故事而无需一遍又一遍地重复相同的 props。更重要的是,它测试了组件集成。如果您重命名其中一个 Task
组件的 props,那将导致 TaskList
的测试用例失败。

到目前为止,我们只处理了通过 props 接受数据和回调的组件。当您的组件连接到 API 或具有内部状态时,事情会变得更加棘手。接下来,我们将研究如何隔离和测试此类连接的组件。
有状态的复合组件
InboxScreen
使用自定义 Hook 从 Taskbox API 获取数据并管理应用程序状态。与单元测试非常相似,我们希望将组件与真实的后端分离,并隔离测试功能。

这就是 Storybook 插件的用武之地。它们允许您模拟 API 请求、状态、上下文、提供程序以及组件依赖的任何其他内容。The Guardian 和 Sidewalk Labs (Google) 的团队使用它们来隔离构建整个页面。
对于 InboxScreen,我们将使用 Mock Service Worker (MSW) 在网络级别拦截请求并返回模拟响应。
安装 msw 及其 storybook 插件。
yarn add -D msw msw-storybook-addon
然后,在您的 public 文件夹中生成一个新的 service worker。
npx msw init public/
通过将此代码添加到您的 ./storybook/preview.js
文件中,在 Storybook 中启用 MSW 插件
import { addDecorator } from '@storybook/react';
import { initialize, mswDecorator } from 'msw-storybook-addon';
initialize();
addDecorator(mswDecorator);
最后,重启 yarn storybook
命令。我们都已准备好在故事中模拟 API 请求。
InboxScreen
调用 useTasks
Hook,该 Hook 反过来从 /tasks
端点获取数据。我们可以使用 msw
参数指定模拟响应。请注意,您可以为每个故事返回不同的响应。
// InboxScreen.stories.js
import React from 'react';
import { rest } from 'msw';
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: [
rest.get('/tasks', (req, res, ctx) => {
return res(ctx.json(TaskListDefault.args));
}),
],
};
export const Error = Template.bind({});
Error.args = {
error: 'Something',
};
Error.parameters = {
msw: [
rest.get('/tasks', (req, res, ctx) => {
return res(ctx.json([]));
}),
],
};

状态有许多不同的形式。一些应用程序使用 Redux 和 MobX 等库或通过发出 GraphQL 查询来全局跟踪状态位。或者他们可能使用容器组件。Storybook 足够灵活,可以支持所有这些场景。有关更多信息,请参阅:用于管理数据和状态的 Storybook 插件。
隔离构建组件降低了开发的复杂性。您不必启动后端、以用户身份登录并在 UI 中点击来调试一些 CSS。您可以将所有内容设置为一个故事并开始进行。您甚至可以对这些故事运行自动回归测试。
捕获回归
在我之前的可视化测试文章中,我们花了一些时间设置 Chromatic 并回顾了基本工作流程。Chromatic 捕获每个故事的快照,并将其与现有基线进行比较。您将看到一个视觉差异,您可以批准或拒绝它。
现在我们已经为所有复合组件编写了故事,我们可以通过运行以下命令来执行可视化测试
npx chromatic --project-token=<project-token>
您应该看到一个差异,其中包括 TaskList 和 InboxScreen 的故事。

现在尝试更改 Task 组件中的某些内容,例如字体大小或背景颜色。然后提交更改并重新运行 Chromatic。

应用程序的树状结构意味着对 Task 组件的任何调整也会被更高级别组件的测试捕获。测试复合组件使您能够在部署到生产环境之前捕获错误。
结论
鉴于现代应用程序的规模,开发人员不可能知道组件被使用的所有不同位置。因此,您经常最终意外地发布错误。这会拖累您的速度——在生产环境中修复这些错误需要 5-10 倍 的时间。组合测试使我们能够了解小的更改对较大系统的潜在影响。您可以在错误滚雪球般变成重大回归之前捕获它们。
接下来,我们将深入探讨交互测试。当用户选中一个任务时,您如何确保触发了合适的事件并且状态已正确更新?加入邮件列表,以便在发布更多 UI 测试文章时收到通知。
阻止错误连锁反应!
— Storybook (@storybookjs) 2021年7月14日
应用程序是组件的互连树。如果一个失败,它周围的其他组件也会崩溃。
组合测试使您能够了解每个小错误对整个系统的影响。并将错误扼杀在萌芽状态。https://#/bqMyQD8MNR pic.twitter.com/nrdjDWeNWw