测试复合组件
2021 年 1 月,特斯拉召回了 15.8 万辆汽车,因为一个模块——显示器——发生故障。显示控制台损坏后,无法使用倒车摄像头、转向灯或驾驶辅助系统。这大大增加了碰撞的风险。
一个有缺陷的模块升级成了重大故障。
UI 也面临类似的挑战,因为应用就像汽车一样,是由相互连接的各个部分组成的网络。一个组件中的 bug 会影响其周围的所有其他组件。更不用说应用中使用它的每个部分了。测试 UI 组件如何组合有助于防止此类 bug。
测试 UI 中更复杂的部分很棘手。它们是通过组合许多更简单组件创建的,并且也连接到应用状态。在本章中,我们将探讨如何隔离复合组件并对其应用视觉测试。在此过程中,您将学习如何模拟数据和模拟应用逻辑。以及测试组件集成的方法。
小 bug 最终会导致应用崩溃
应用是通过将组件相互连接构建的。这意味着一个元素中的 bug 会影响其邻居。例如,重命名一个 prop 会中断从父组件到子组件的数据流。或者 UI 元素中不正确的 CSS 通常会导致布局损坏。
考虑来自Storybook 设计系统的 Button 组件。它在多个页面中被无数次使用。Button
中的一个 bug 会无意中导致所有这些页面出现 bug。换句话说,一个故障可以呈指数级累积。随着您在组件层级结构中向页面级别移动,这些 bug 的影响会增加。因此,我们需要一种方法来尽早捕获此类连锁问题并找出根本原因。
组合测试
视觉测试通过在真实浏览器中捕获和比较 stories 的图像快照来捕获 bug。这使得它们成为发现 UI 变化和识别根本原因的理想工具。以下是流程的快速回顾:
- 🏷 隔离组件。使用 Storybook 一次测试一个组件。
- ✍🏽 写出测试用例。使用 props 重现每个组件状态。
- 🔍 手动验证每个测试用例的外观。
- 📸 使用视觉回归测试自动捕获 bug。
组合测试就是对树中由多个更简单组件组成的更高级别的“复合”组件运行视觉测试。这样您就可以量化任何更改可能对整个应用产生的影响。并确保整个系统正常工作。
关键区别在于,复合组件跟踪应用状态并将行为向下传递给子组件。在编写测试用例时,您必须考虑到这些。
让我们通过为 TaskList
组件编写测试来了解这个过程的实际应用,该组件显示属于用户的完整任务列表。
它将置顶任务移到列表顶部。并且有加载状态和空状态。我们将从为所有这些场景编写 stories 开始。
创建一个 story 文件,注册 TaskList
组件并添加一个用于默认情况的 story。
import TaskList from './TaskList';
import * as TaskStories from './Task.stories';
export default {
component: TaskList,
title: 'TaskList',
argTypes: {
...TaskStories.argTypes,
},
};
export const 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 定义 story 输入的机制。可以将它们视为与框架无关的 props。在组件级别定义的 Args 会自动传递给每个 story。在我们的例子中,我们使用Actions 插件定义了三个事件处理程序。
当您与 TaskList
交互时,这些模拟操作将显示在插件面板中。让您可以验证组件是否正确连接。
组合 args
您可以将组件组合起来创建新的 UI,同样也可以组合 args 来创建新的 stories。复合组件的 args 通常会包含其子组件的 args。
事件处理程序的 args 已在 Task stories 文件中定义,我们可以重用它们。同样,我们也可以使用默认 story 中的 args 来创建置顶任务 story。
import TaskList from './TaskList';
import * as TaskStories from './Task.stories';
export default {
component: TaskList,
title: 'TaskList',
argTypes: {
...TaskStories.argTypes,
},
};
export const 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 = {
args: {
tasks: [
{
id: '6',
title: 'Draft monthly blog to customers',
state: 'TASK_PINNED',
},
...Default.args.tasks.slice(0, 5),
],
},
};
export const Loading = {
args: {
tasks: [],
loading: true,
},
};
export const Empty = {
args: {
...Loading.args,
loading: false,
},
};
通过 args 组合来塑造 stories 是一种强大的技术。它使我们无需一遍又一遍地重复相同的数据即可编写 stories。更重要的是,它测试了组件集成。如果您重命名 Task
组件的其中一个 prop,那将导致 TaskList
的测试用例失败。
到目前为止,我们只处理了通过 props 接收数据和回调的组件。当您的组件连接到 API 或具有内部状态时,事情会变得更复杂。接下来,我们将探讨如何隔离和测试此类连接的组件。
有状态的复合组件
InboxScreen
使用自定义 hook 从 Taskbox API 获取数据并管理应用状态。就像单元测试一样,我们希望将组件与真实的后端分离,并单独测试其功能。
这就是 Storybook 插件的作用。它们允许您模拟 API 请求、状态、上下文、提供者以及组件依赖的任何其他内容。《卫报》和Sidewalk Labs(谷歌)的团队使用它们来独立构建整个页面。
对于 InboxScreen,我们将使用Mock Service Worker (MSW) 插件来在网络层面拦截请求并返回模拟响应。
这已在介绍章中提供的模板中。我们需要对其进行设置。让我们看看如何设置。
运行以下命令在您的 public
文件夹中生成一个新的 service worker。
yarn init-msw
💡 公共目录可能因项目而异。对于自定义配置,我们建议阅读 MSW 的文档以了解更多信息。要在 Storybook 中看到更改,您需要更新 .storybook/main.js
中的staticDirs
配置元素。
在您的 .storybook/preview.js
文件中启用 MSW 插件。
import '../src/index.css';
+ import { initialize, mswLoader } from 'msw-storybook-addon';
+ // Initialize MSW
+ initialize();
/** @type { import('@storybook/react').Preview } */
const preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
+ loaders: [mswLoader],
};
export default preview;
最后,重新启动 yarn storybook
命令。然后我们就可以开始在 stories 中模拟 API 请求了。
InboxScreen
调用 useTasks
hook,后者从 /tasks
endpoint 获取数据。我们可以使用 msw
参数指定模拟响应。请注意,您可以为每个 story 返回不同的响应。
import { http, HttpResponse } from 'msw';
import InboxScreen from './InboxScreen';
import { Default as TaskListDefault } from './components/TaskList.stories';
export default {
component: InboxScreen,
title: 'InboxScreen',
};
export const Default = {
parameters: {
msw: {
handlers: [
http.get('/tasks', () => {
return HttpResponse.json(TaskListDefault.args);
}),
],
},
},
};
export const Error = {
args: {
error: 'Something',
},
parameters: {
msw: {
handlers: [
http.get('/tasks', () => {
return HttpResponse.json([]);
}),
],
},
},
};
状态有许多不同的形式。一些应用使用 Redux 和 MobX 等库来全局跟踪部分状态。或者通过进行 GraphQL 查询。或者它们可能使用容器组件。Storybook 足够灵活,可以支持所有这些场景。有关更多信息,请参阅:用于管理数据和状态的 Storybook 插件。
独立构建组件可以减少开发的复杂性。您无需启动后端、以用户身份登录并在 UI 中点击,只为了调试一些 CSS。您可以将所有内容设置为一个 story,然后开始工作。甚至可以在这些 stories 上运行自动化回归测试。
捕获回归
在上一章中,我们设置了 Chromatic 并回顾了基本工作流程。现在我们已经为所有复合组件准备好了 stories,可以通过运行以下命令来执行视觉测试:
npx chromatic --project-token=<project-token>
您将看到一个包含 TaskList 和 InboxScreen 的 diff。
现在尝试更改 Task 组件中的某些内容,例如字体大小或背景颜色。然后提交并重新运行 Chromatic。
应用的树状结构意味着对 Task 组件的任何调整也会被更高级别组件的测试捕获。组合测试使您能够了解每个微小更改的潜在影响。
验证组件功能
接下来,我们将超越外观,进入测试交互。当用户完成一个任务时,如何确保触发了合适的事件并且状态更新正确?