返回博客

视觉测试: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".

视觉测试 101

在我们深入探讨视觉测试为何如此出色的原因之前,它是什么以及它是如何工作的?

视觉测试是一种快照测试,它在代码更改之前之后比较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

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

视觉测试不仅断言按钮包含正确的文本,还断言按钮是蓝色的,具有圆角,以相同的字体呈现,等等。它们通过编写一行显式断言就能做到这一点。

使用Visual Tests插件,测试变得如此简单

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来测试这一点,该故事设置了带有输入的购物车,但实际上不包含任何显式的测试逻辑

// 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();
});

当然,我们可以通过一个辅助函数来缩短所有这些代码,以检查每个购物车项目,但当我们为这类测试编写和维护辅助函数时,我们就已经输了。

现在将这个单一测试乘以整个应用程序,其中可能包含数百个不同复杂度的组件。维护这类测试简直是噩梦。

相比之下,为数百个组件编写故事并进行视觉测试是可行的,而世界上最好的前端团队已经在这样做了。

测试UX,而不是实现细节

测试专家Cory House最近对某人的观点评论说:“自动化测试就像往代码上浇筑混凝土。”上一节中的RTL代码正是人们在自动化测试中抱怨的“混凝土”。

为了避免混凝土,Cory建议“测试UX,而不是实现细节”。而测试UX正是视觉测试所提供的。此外,视觉快照比代码更容易维护:正如我们上面所见,更新测试就像在您的故事呈现出期望的状态时按下按钮接受新的基线快照一样简单。

由于Storybook还支持RTL操作和查询,您拥有所需的全部功能,可以根据需要在任何细节级别进行测试,以获得对代码的信心。

Storybook中的视觉测试

在Storybook,我们对视觉测试深信不疑,因此我们将其作为一项一流的功能包含在内。Storybook的Visual Test Addon由Chromatic支持,后者是世界上最好的视觉测试基础设施。

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

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

Chromatic提供PR检查,以指示PR是否存在视觉更改。当测试失败时,用户可以点击进入一个高效的UI来审查视觉更改。直到现在,PR检查一直是使用Chromatic和其他类似视觉测试服务的首选工作流程。

Storybook的Visual Tests Addon是此工作流程的一项创新性新功能,它将Chromatic的功能置于Storybook本身之中。这使您可以在开发过程中按需运行视觉测试,而无需推送代码、运行CI并等待一堆不相关的检查。

这是一个惊人的工作流程。现在,从您的组件工作坊中,您可以

  1. 发起视觉测试
  2. 过滤侧边栏以突出显示视觉差异
  3. 在Storybook中审查和处理这些更改
Screenshot of a Storybook showing running visual tests and the highlighted test failures in the sidebar

Visual Tests Addon使捕获UI错误比以往任何时候都更快,并在您构建组件时保持流程。我们相信这是UI开发“圣杯”的重要一步。

立即试用

Storybook的Visual Test Addon已包含在新安装的Storybook中

npx storybook@latest init

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

npx storybook@latest upgrade

下一步是什么?

Visual Tests Addon已稳定,并于今日在Storybook 8中可用。我们正在考虑以下增强功能

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

For an overview of projects we’re considering and actively working on, please check out Storybook’s roadmap.

加入 Storybook 邮件列表

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

7,468开发者及更多

我们正在招聘!

加入 Storybook 和 Chromatic 团队。构建被数十万开发人员在生产中使用的工具。远程优先。

查看职位

热门帖子

State of JS 2023:从急转直下的局面中奋起直追

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

Storybook 8.2

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

交互式故事生成

无需离开浏览器,即可在几秒钟内创建您的第一个故事!
loading
Valentin Palkovic
加入社区
7,468开发者及更多
原因为什么选择 Storybook组件驱动的 UI
文档指南教程更新日志遥测
社区插件参与进来博客
展示探索项目组件词汇表
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI