返回博客

视觉测试:UI 开发中的“魔法”

更高的信心,更少的维护

loading
Michael Shilman
@mshilman
最后更新于

在 UI 开发中,确保界面看起来正确与确保功能正常工作同等重要。视觉测试就是解决这个问题的图片快照测试。

然而,有些出人意料的是,它们还可以取代许多 UI 单元测试中最脆弱的部分:对 UI 细节的断言。在许多情况下,这可以完全取代单元测试,让您用更少的代码测试更多内容。

本文内容包括

如果您仍在对组件进行单元测试,请继续阅读,了解一种更好的 UI 开发方法。

Illustration of a simplified Storybook on the left, labeled "Write code". There's an arrow pointing right labeled "Detect bugs". On the right is that same simplified Storybook with highlighted stories in the sidebar, labeled "Visual test". There's an arrow pointing left labeled "Fix".

视觉测试入门

在深入探讨视觉测试的妙处之前,先来了解一下它是什么以及它是如何工作的?

视觉测试是一种快照测试,它会比较代码更改之前之后 UI 组件的图像快照。如果快照不匹配,测试就会失败。

  • 差异是预期内的,需要更新基线(之前)图像
  • 差异是意外的,用户应该去修复代码。

以下是该过程在实践中的样子

A workflow diagram with 5 steps. 1) Baseline; Create a story and save a snapshot as the baseline. 2) Update; Update your component code. 3) Run visual test; Take a snapshot of your changes and compare with the baseline. 4) Accept or deny; Approve, if the change is intentional, or deny the change. 5) New baseline; When the test is accepted, the baseline is updated.

更少的代码,更好的测试

视觉测试非常巧妙,但我们为何认为它是测试 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 内部的样子

Screenshot of Storybook, showing the example Button component story and its passing test

使用这样的测试,我们只断言一件事:按钮的文本。

视觉测试不仅断言按钮包含正确的文本,还断言按钮是蓝色的、有圆角、使用相同的字体渲染等等。而这一切都无需编写任何显式断言。

借助视觉测试插件,该测试变得如此简单

export const CustomText = {
  args: { children: 'Click me!' },
};

在下面的示例中,我不小心在全局 CSS 中引入了一个错误,该错误剥夺了 Button 的大部分样式。这会通过 RTL 的功能测试,但我们的视觉测试会捕捉到差异并将其显示为变更

Screenshot of Storybook showing the example Button story and its failing visual test

实际示例

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

Screenshot of Storybook showing the ShoppingCartMenu component's "With Items" story

从功能上讲,我们希望测试购物车中的所有商品都正确显示,并且由于购物车中有商品,结账按钮已启用。

使用视觉测试,我们可以通过名为 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 建议“测试用户体验,而非实现细节”。而测试用户体验正是视觉测试所提供的。更重要的是,视觉快照比代码更容易维护:如上所示,更新测试就像在 story 渲染到所需状态时按下按钮接受新的基线快照一样简单。

由于 Storybook 也支持 RTL actions 和 queries,您可以根据需要进行任何级别的详细测试,以增强对代码的信心。

Storybook 中的视觉测试

在 Storybook,我们坚信视觉测试的价值,并将其作为一项核心功能。Storybook 的视觉测试插件由全球最佳视觉测试基础架构提供商Chromatic 提供支持。

Chromatic 通过比较代码更新前后图像快照来识别更改,并突出显示差异供审查。它在云中并行运行数千个测试,仅需几十秒,涵盖多种浏览器(Chrome、Safari、Firefox、Edge)、视口、主题和国际化语言环境。

Workflow diagram with three steps. 1) Push code. 2) Detect UI changes. 3) Get PR checks

Chromatic 提供 PR 检查,用于指示 PR 关联的视觉更改。当测试失败时,用户可以点击进入一个高效的 UI 来审查视觉更改。在此之前,PR 检查是使用 Chromatic 和其他类似视觉测试服务的主要工作流程。

Storybook 的视觉测试插件是对现有工作流程的一次全新创新,它将 Chromatic 的强大功能置于 Storybook 内部。这让您可以在开发时按需运行视觉测试,无需推送代码、运行 CI 并等待大量无关检查。

这是一个令人惊叹的工作流程。现在,在组件工作区内即可实现:

  1. 启动视觉测试
  2. 过滤侧边栏以突出显示视觉差异
  3. 在 Storybook 内部审查和解决这些更改
Screenshot of a Storybook showing running visual tests and the highlighted test failures in the sidebar

视觉测试插件使您比以往更快地捕获 UI 错误并在构建组件时保持流畅。我们相信这是迈向 UI 开发“圣杯”的重要一步。

立即试用

新安装的 Storybook 中已包含 Storybook 的视觉测试插件

npx storybook@latest init

如果您从旧版本 Storybook 升级,现在会提示您选择是否将插件安装到现有项目中

npx storybook@latest upgrade

接下来?

视觉测试插件目前已稳定并在 Storybook 8 中可用。我们正在考虑以下增强功能:

  1. 全屏审查模式,用于接受和拒绝更改。
  2. 将测试范围限定为当前可见的 story 或组件的能力。
  3. 一个始终开启的“watch mode”,可在您的开发机器上本地运行功能测试,并通过更快的反馈循环补充视觉测试。

有关我们正在考虑和积极进行的项目概述,请查看Storybook 的路线图

加入 Storybook 邮件列表

获取最新消息、更新和发布信息

7,180开发人员及更多

我们正在招聘!

加入 Storybook 和 Chromatic 背后的团队。构建数十万开发者在生产环境中使用的工具。远程优先。

查看职位

热门文章

State of JS 2023:从猛烈打击中反击

Storybook 如何使用调查来指导开发
loading
Michael Shilman

Storybook 8.2

迈向无妥协的组件测试
loading
Michael Shilman

交互式 story 生成

无需离开浏览器,几秒钟内创建您的第一个 story!
loading
Valentin Palkovic
加入社区
7,180开发人员及更多
原因为何选择 Storybook组件驱动型 UI
文档指南教程更新日志遥测
社区插件参与其中博客
案例展示探索项目组件词汇表
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI