返回博客

可视化测试: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 插件由 Chromatic 提供支持,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 插件 是此工作流程的一种新的创新性转变,它将 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 插件使捕获 UI 错误和在构建组件时保持流畅比以往任何时候都更快。我们相信这是朝着 UI 开发“圣杯”迈出的重要一步。

立即尝试

新的 Storybook 安装中包含 Storybook 的 Visual Test 插件

npx storybook@latest init

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

npx storybook@latest upgrade

下一步是什么?

Visual Tests 插件稳定且今天在 Storybook 8 中可用。我们正在考虑以下增强功能

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

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

加入 Storybook 邮件列表

获取最新的新闻、更新和发布

6,730位开发者以及更多

我们正在招聘!

加入 Storybook 和 Chromatic 背后的团队。构建被成千上万的开发者在生产中使用的工具。远程优先。

查看职位

热门文章

JS 2023 状态:从猛烈的左勾拳中反击

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

Storybook 8.2

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

交互式故事生成

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

特别感谢 Netlify CircleCI