
视觉测试:UI 开发中的“魔法”
更高的信心,更少的维护

在 UI 开发中,确保界面看起来正确与确保功能正常工作同等重要。视觉测试就是解决这个问题的图片快照测试。
然而,有些出人意料的是,它们还可以取代许多 UI 单元测试中最脆弱的部分:对 UI 细节的断言。在许多情况下,这可以完全取代单元测试,让您用更少的代码测试更多内容。
本文内容包括
如果您仍在对组件进行单元测试,请继续阅读,了解一种更好的 UI 开发方法。

视觉测试入门
在深入探讨视觉测试的妙处之前,先来了解一下它是什么以及它是如何工作的?
视觉测试是一种快照测试,它会比较代码更改之前和之后 UI 组件的图像快照。如果快照不匹配,测试就会失败。
- 差异是预期内的,需要更新基线(之前)图像
- 差异是意外的,用户应该去修复代码。
以下是该过程在实践中的样子

更少的代码,更好的测试
视觉测试非常巧妙,但我们为何认为它是测试 UI 的一种根本更好的方法?简而言之,视觉测试比单元测试更容易编写和维护。同时,它们提供了更高的信心,因为它们测试的内容更多。
考虑一个使用React Testing Library (RTL) 的简单示例,这是在 Jest 和 Vitest 等测试运行器中对组件进行单元测试的最常用方法。
// Button.test.js
import { render, screen } from '@testing-library/react';
import Button from './Button';
it('uses custom text for the button label', () => {
render(<Button>Click me!</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click me!');
})
此测试挂载 Button
组件,然后断言按钮标签的文本内容。Playwright CT 和 Cypress CT 等工具也使用类似的语法和结构。
Storybook 的语法略有不同,但思路相同。以下是与 RTL 示例等效的代码
// Button.stories.js
import Button from './Button';
export default { component: Button };
export const CustomText = {
args: { children: 'Click me!' },
play: async ({ canvasElement }) => {
await expect(canvasElement).toHaveTextContent('Click me!')
},
};
以下是 Storybook 内部的样子

使用这样的测试,我们只断言一件事:按钮的文本。
视觉测试不仅断言按钮包含正确的文本,还断言按钮是蓝色的、有圆角、使用相同的字体渲染等等。而这一切都无需编写任何显式断言。
借助视觉测试插件,该测试变得如此简单
export const CustomText = {
args: { children: 'Click me!' },
};
在下面的示例中,我不小心在全局 CSS 中引入了一个错误,该错误剥夺了 Button 的大部分样式。这会通过 RTL 的功能测试,但我们的视觉测试会捕捉到差异并将其显示为变更

实际示例
少写一行断言可能看起来没什么大不了,但在实际项目中,这些好处会迅速累积。考虑一个像Mealdrop 购物车这样的组件

从功能上讲,我们希望测试购物车中的所有商品都正确显示,并且由于购物车中有商品,结账按钮已启用。
使用视觉测试,我们可以通过名为 WithItems
的 story 来测试这一点,该 story 设置了带有输入的购物车,但实际上不包含任何显式的测试逻辑
// ShoppingCartMenu.stories.js
import { ShoppingCartMenu } from './ShoppingCartMenu'
export default { component: ShoppingCartMenu };
export const WithItems = {
args: {
cartItems: [ /* items */ ],
totalPrice: 1200
},
}
如果我们不相信已启用的按钮会在 UI 中以不同方式渲染,我们可以扩展该测试来定义 WithItemsEnabled
,它专门验证按钮未被禁用
// ShoppingCartMenu.stories.js
export const WithItemsEnabled = {
...WithItems,
play: async ({ canvasElement }) => {
const checkout = await findByRole(canvasElement, 'button');
await expect(checkout).not.toBeDisabled();
},
}
现在想象一下仅使用 RTL 编写相同的测试。我们会想测试购物车中的每个商品是否以正确的数量显示、总计是否正确等等。
// ShoppingCartMenu.test.js
it('renders correctly with items', () => {
render(<ShoppingCartMenu cartItems={[ /* items */ ]} totalPrice={1200} />);
const fries = await screen.findByText(/^Fries$/);
expect(getByText(fries.parentElement, '€2.50')).toBeInTheDocument();
// More assertions here
const cheeseburger = await screen.findByText(/^Cheeseburger$/);
expect(getByText(cheeseburger.parentElement, '€8.50')).toBeInTheDocument();
// More assertions here
/*
*
*
* Dozens of lines omitted here,
* for everybody's sanity.
*
*
*/
const checkout = screen.getByRole('button');
expect(checkout).not.toBeDisabled();
});
当然,我们可以使用一个辅助函数来检查每个购物车商品来缩短这一切,但是当我们需要为这样的测试编写和维护辅助函数时,我们已经输了。
现在将这个单一测试扩展到你的整个应用程序,其中可能包含数百个不同复杂度的组件。维护这些类型的测试简直是一场噩梦。
相比之下,为数百个组件编写 story 并对其进行视觉测试是可行的,而且世界上最好的前端团队已经在这样做了。
测试用户体验,而非实现细节
测试大师 Cory House 最近评论了某人的观点,即“自动化测试就像在代码上浇筑混凝土一样”。前一节中的 RTL 代码正是人们在自动化测试中抱怨的“混凝土”。
“自动化测试就像在代码上浇筑混凝土一样。”
— Cory House (@housecor) 发布于2024年5月26日
我不同意。
代码是可塑的。混凝土则不是。
为了避免测试感觉像“在代码上浇筑混凝土”
1. 在需求明确之前不要编码。
2. 避免要求 100% 代码覆盖率。
3. 测试用户体验,而非…
为了避免混凝土,Cory 建议“测试用户体验,而非实现细节”。而测试用户体验正是视觉测试所提供的。更重要的是,视觉快照比代码更容易维护:如上所示,更新测试就像在 story 渲染到所需状态时按下按钮接受新的基线快照一样简单。
由于 Storybook 也支持 RTL actions 和 queries,您可以根据需要进行任何级别的详细测试,以增强对代码的信心。
Storybook 中的视觉测试
在 Storybook,我们坚信视觉测试的价值,并将其作为一项核心功能。Storybook 的视觉测试插件由全球最佳视觉测试基础架构提供商Chromatic 提供支持。
Chromatic 通过比较代码更新前后图像快照来识别更改,并突出显示差异供审查。它在云中并行运行数千个测试,仅需几十秒,涵盖多种浏览器(Chrome、Safari、Firefox、Edge)、视口、主题和国际化语言环境。

Chromatic 提供 PR 检查,用于指示 PR 关联的视觉更改。当测试失败时,用户可以点击进入一个高效的 UI 来审查视觉更改。在此之前,PR 检查是使用 Chromatic 和其他类似视觉测试服务的主要工作流程。
Storybook 的视觉测试插件是对现有工作流程的一次全新创新,它将 Chromatic 的强大功能置于 Storybook 内部。这让您可以在开发时按需运行视觉测试,无需推送代码、运行 CI 并等待大量无关检查。
这是一个令人惊叹的工作流程。现在,在组件工作区内即可实现:
- 启动视觉测试
- 过滤侧边栏以突出显示视觉差异
- 在 Storybook 内部审查和解决这些更改

视觉测试插件使您比以往更快地捕获 UI 错误并在构建组件时保持流畅。我们相信这是迈向 UI 开发“圣杯”的重要一步。
立即试用
新安装的 Storybook 中已包含 Storybook 的视觉测试插件
npx storybook@latest init
如果您从旧版本 Storybook 升级,现在会提示您选择是否将插件安装到现有项目中
npx storybook@latest upgrade
接下来?
视觉测试插件目前已稳定并在 Storybook 8 中可用。我们正在考虑以下增强功能:
- 全屏审查模式,用于接受和拒绝更改。
- 将测试范围限定为当前可见的 story 或组件的能力。
- 一个始终开启的“watch mode”,可在您的开发机器上本地运行功能测试,并通过更快的反馈循环补充视觉测试。
有关我们正在考虑和积极进行的项目概述,请查看Storybook 的路线图。
我们坚信视觉测试以及我们的新视觉测试插件是测试您的 UI 的最佳方式,这能在您开发时给予您信心。
— Storybook (@storybookjs) 发布于2024年6月20日
我们写了一篇文章来解释原因。https://#/p6IbzHh1B0