特斯拉刚刚召回了15.8万辆汽车,因为一个模块--显示屏--发生了故障。在显示控制台损坏的情况下,你无法访问备用摄像头、转向灯或驾驶辅助系统。这大大增加了车祸的风险:
一个有缺陷的模块升级为一个重大故障。
UIs也面临着类似的挑战,因为应用程序和汽车一样,是一个相互连接的零件网络。一个部件的错误会影响到它周围的所有其他部件。更不用说应用程序中使用的每一个部分了。测试UI组件是如何组成的有助于你防止这种错误。
在上一篇文章中,我们学习了如何使用Storybook来孤立地构建组件,编写可视化测试 ,并使用Chromatic来自动捕捉回归。然而,测试UI中更复杂的部分是很棘手的。它们是由许多较简单的组件组合而成的,而且还与应用程序的状态相连接。
这篇文章教你如何隔离和应用视觉测试到复合组件。在此过程中,你将了解到模拟数据和模拟应用逻辑。以及测试组件集成的方法。
小错误最终会破坏应用程序
应用程序是通过组件之间的相互插入来构建的。这意味着一个元素的错误会影响到它的邻居。例如,重命名一个道具会破坏从父组件到子组件的数据流。或者一个UI元素中不正确的CSS常常导致布局的破坏。


考虑一下Storybook设计系统中的Button组件。它在多个页面上被使用了无数次。Button中的一个错误会在无意中导致所有这些页面的错误。换句话说,一次失败会带来成倍的损失。当你在组件的层次结构上向页面的层次移动时,这些错误的影响就会增加。因此,我们需要一种方法来及早发现这种级联的问题,并找出根本原因。

组成测试
视觉测试通过捕捉和比较真实浏览器中故事的图像快照来捕捉错误。这使得它们成为发现UI变化和确定根本原因的理想选择。下面是对这个过程的快速提醒。
- 🏷隔离组件。使用Storybook,一次测试一个组件。
- ✍🏽 写出测试案例。使用道具再现每个组件的状态。
- 🔍手动验证每个测试用例的外观。
- 📸 使用可视化回归测试自动捕捉错误 。
组成测试是关于在树中较高的 "复合 "组件上运行视觉测试,这些组件是由几个较简单的组件组成的。这样你就可以量化任何变化可能对整个应用产生的影响。并确保系统作为一个整体工作。
这个关键的区别是,复合组件跟踪应用程序的状态,并将行为传递到树上。在编写测试用例时,你必须考虑到这些。
让我们看看这个过程的实际情况。我们将使用我在第二部分介绍的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的机制,用于定义故事的输入。可以把它们看作是与框架无关的道具。在组件级别定义的参数会自动传递到每个故事中。在我们的例子中,我们使用Actions插件定义了三个事件处理程序。
当你与TaskList ,这些模拟的动作将显示在addons面板上。让你可以验证组件的接线是否正确:

组建args
就像你组合组件来创建新的UI一样,你可以组合args来创建新的故事。典型的情况是,一个复合组件的args甚至会结合其子组件的args。
事件处理程序的args已经在任务故事文件中定义了,我们可以重新使用。同样地,我们也可以使用默认故事中的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构成来塑造故事是一种强大的技术。它允许我们在编写故事时不需要重复相同的道具。而且更重要的是,它可以测试组件的整合。如果你重新命名一个Task 组件的道具,这将导致TaskList 的测试案例失败:

到目前为止,我们只处理了通过props接受数据和回调的组件。当你的组件与API连接或有内部状态时,事情就变得棘手了。接下来我们将看看如何隔离和测试这种连接的组件。
有状态的复合组件
InboxScreen 使用自定义钩子从Taskbox API获取数据并管理应用程序的状态。与单元测试一样,我们希望将组件从真正的后台分离出来,并对功能进行隔离测试:

收件箱屏幕
这就是Storybook附加组件的作用。它们允许你模拟API请求、状态、上下文、供应商以及其他任何你的组件所依赖的东西。卫报》和Sidewalk实验室(谷歌)的团队使用它们来单独构建整个页面。
对于InboxScreen,我们将使用Mock Service Worker(MSW)在网络层面拦截请求并返回模拟响应。
安装MSW和它的storybook addon:
yarn add -D msw msw-storybook-addon
然后,在你的公共文件夹中生成一个新的服务工作者:
npx msw init public/
在Storybook中启用MSW插件,将其添加到你的./storybook/preview.js 文件:
import { addDecorator } from '@storybook/react';
import { initialize, mswDecorator } from 'msw-storybook-addon';
initialize();
addDecorator(mswDecorator);
最后,重新启动yarn storybook 命令。然后我们就可以在故事中模拟API请求了。
InboxScreen 调用 钩子,然后从 端点获取数据。我们可以使用 参数指定模拟响应。注意你可以为每个故事返回不同的响应。useTasks /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管理数据和状态的附加组件。
孤立地构建组件减少了开发的复杂性。你不必为了调试一些CSS而启动后端,以用户身份登录并点击UI。你可以把它设置成一个故事,然后开始工作。你甚至可以在这些故事上运行自动回归测试。
捕捉回归
在我之前的可视化测试文章中,我们花了一些时间来设置Chromatic,并复习了基本的工作流程。Chromatic捕捉每个故事的快照,并将其与现有的基线进行比较。你会看到一个可视化的差异,你可以批准或拒绝。
现在我们有了所有复合组件的故事,我们可以通过运行来执行可视化测试:
npx chromatic --project-token=<project-token>
你应该看到一个包括TaskList和InboxScreen的故事的差异:

现在试着改变任务组件中的一些东西,比如字体大小或背景颜色。然后提交并重新运行Chromatic。

应用程序的树状特性意味着对任务组件的任何调整也会被更高级别的组件的测试所捕获。测试复合组件可以让你在部署到prod之前抓住bug。
总结
考虑到现代应用程序的规模,开发人员不可能知道一个组件被使用的所有不同地方。因此,你经常会意外地出现bug。这使你陷入困境--在生产中修复这些bug需要5-10倍的时间。组成测试使我们能够了解小改动对大系统的潜在影响。你可以在bug滚雪球似的变成重大退步之前抓住它们。
接下来,我们将进入测试交互。当用户勾选一个任务时,你如何确保合适的事件被触发,并且该状态被正确更新?加入邮件列表,以便在更多的UI测试文章发布时得到通知。