返回博客

Storybook 中的视觉测试

了解如何自动定位 UI 错误

loading
Varun Vachhar
@winkerVSbecks
最近更新

上个月,超过 14,000 名开发者阅读了视觉测试手册 这是 Storybook 维护者编写的一份关于检测 UI 外观错误的详尽指南。

视觉测试日益增长的兴趣源于交付无错误的 UI 非常困难。过去,开发者使用单元测试和快照测试来扫描 HTML 代码块中的错误。但这些方法无法代表用户实际看到的内容,因此错误从未真正消失。

视觉测试通过在真实浏览器中捕获和比较图像快照来发现错误。它可以让你自动化检查 UI 外观是否正确的流程。

《视觉测试手册》是一本关于测试 UI 外观的深度指南,其中包含了 BBC、Adobe、Target 等领先工程团队的经验。本文总结了该指南中概述的基本工具和技术。

什么是视觉错误?

视觉错误无处不在。元素被截断、颜色或字体样式不正确、布局损坏,以及缺少错误状态等。

现在每家公司都是一家软件公司。这意味着每家公司都有责任维护一个 UI。但如果你和我一样,你可能会注意到,公司似乎永远没有足够的人力来持续监控其 UI 的每一个部分。

视觉错误是 UI 外观中无意的错误,这些错误会使 UI 看起来不可靠。它们是那些用肉眼容易发现,但常用测试方法却无法捕获的回归问题。

大多数测试旨在验证逻辑。这很合理。你运行一个函数,获取其输出,然后检查它是否正确。计算机非常擅长验证数据。但外观呢?

好,这个问题有两个层面。

1. 外观是否正确?

以这个 Task 组件为例。它在不同状态下看起来不同。我们显示一个已选中(或未选中)的复选框、关于任务的一些信息以及一个置顶按钮。当然,还有所有相关的样式。

第一个挑战只是验证组件在所有这些场景下的外观。这需要大量调整 props & state 来设置和测试每种情况。哦,而且计算机并不能真正告诉你它是否符合规范。你需要,作为开发者,亲自动手进行视觉检查。

2. 它仍然外观正确吗?

你第一次构建时就做对了。它在所有状态下都看起来不错。但变化是开发过程中的自然现象。错误不可避免地会悄悄溜入。对于界面来说尤其如此。微小的 CSS 调整都可能破坏组件或其某个状态。

你不可能在每次进行更改时都手动检查整个 UI 的范围。你需要更自动化的方法。

视觉测试

视觉测试让你可以通过一个统一的工作流程来解决这两个任务。它是你在构建组件时以及在迭代发布新功能时,验证组件外观的过程。

视觉测试工作流程如下所示

  1. 🏷 隔离组件。使用 Storybook 来一次专注于测试一个组件。
  2. ✍🏽 编写测试用例。每种状态都使用 props 和模拟数据来重现。
  3. 🔍 手动验证每个测试用例的外观。
  4. 📸 自动捕获 UI 错误。捕获每个测试用例的快照,并使用基于机器的 diff 来检查回归问题。

视觉测试的核心是将 UI 与应用程序的其余部分(数据、后端、API)隔离开来。这样你就可以单独观察每种状态。然后,你可以手动抽查并自动进行这些状态的回归测试。

教程

让我们详细了解每个步骤。但是,我们需要一些东西来测试。我将使用 Taskbox 应用 作为示例。它是一个类似于 Asana 的任务管理应用。

在这里获取代码以便跟随学习:https://github.com/chromaui/ui-testing-guide-code

请注意,实现细节并不重要,因为我们更关注如何测试这个 UI。这里我们使用 React,但请放心,这些测试概念适用于所有基于组件的框架。

1. 隔离组件

通过一次测试一个组件并为其每种状态编写测试用例,可以更容易地定位错误。传统方法是在组件首次使用的应用程序页面上构建它。这使得模拟和验证所有这些状态变得困难。有一个更好的方法——Storybook。

Storybook 是用于隔离构建组件的行业标准工具。Twitter、Slack、Airbnb、Shopify、Stripe 和 Microsoft 都在使用它。它作为一个小型独立工具与你的应用一起存在,为你提供

  • 📥 一个 沙盒 用于隔离渲染每个组件
  • 🔭 将其所有 状态 作为 故事 可视化
  • 📑 文档记录 每个组件的 props 和使用指南
  • 🗃️ 一个 目录 你的所有组件的目录,以便更轻松地发现

让我们回到那个 Task 组件。隔离它意味着我们只加载并渲染这个组件本身。为此,我们需要 Storybook。

设置 Storybook

我分享的代码库已预配置为使用 Storybook。但是,在其他情况下,你可以在项目根目录中运行此命令来设置 Storybook

npx sb init

Storybook CLI 将尝试确定你正在使用哪个前端框架,并在此基础上进行操作:

  • 📦 添加默认配置并安装所需依赖
  • 🛠 设置运行 Storybook 所需的脚本
  • 📝 添加样板故事(stories)助你入门

然后,我们为 Task 组件创建一个 story 文件。这将该组件注册到 Storybook,并添加一个默认测试用例。

// src/components/Task.stories.js

import React from 'react';
import { Task } from './Task';

export default {
  component: Task,
  title: 'Task',
  argTypes: {
    onArchiveTask: { action: 'onArchiveTask' },
    onTogglePinTask: { action: 'onTogglePinTask' },
    onEditTitle: { action: 'onEditTitle' },
  },
};

const Template = (args) => <Task {...args} />;

export const Default = Template.bind({});
Default.args = {
  task: {
    id: '1',
    title: 'Buy milk',
    state: 'TASK_INBOX',
  },
};

最后,运行以下命令以在开发模式下启动 Storybook。你应该会看到 Task 组件加载出来。

yarn storybook

现在我们准备好编写测试用例了。

2. 编写测试用例

在 Storybook 中,测试用例被称为故事(stories)。一个故事捕获了组件的特定状态——即浏览器中实际渲染的状态。

Task 组件有三种状态——默认、已置顶和已存档。我们将为每种状态添加一个故事。

// src/components/Task.stories.js

import React from 'react';
import { Task } from './Task';

export default {
  component: Task,
  title: 'Task',
  argTypes: {
    onArchiveTask: { action: 'onArchiveTask' },
    onTogglePinTask: { action: 'onTogglePinTask' },
    onEditTitle: { action: 'onEditTitle' },
  },
};

const Template = (args) => <Task {...args} />;

export const Default = Template.bind({});
Default.args = {
  task: {
    id: '1',
    title: 'Buy milk',
    state: 'TASK_INBOX',
  },
};

export const Pinned = Template.bind({});
Pinned.args = {
  task: {
    id: '2',
    title: 'QA dropdown',
    state: 'TASK_PINNED',
  },
};

export const Archived = Template.bind({});
Archived.args = {
  task: {
    id: '3',
    title: 'Write schema for account menu',
    state: 'TASK_ARCHIVED',
  },
};

3. 验证

验证就是在 Storybook 中评估组件的外观。也就是说,它是否符合设计规范。

通常的开发工作流程是

  1. 编辑代码
  2. 将组件设置为适当的状态
  3. 评估其外观

然后重复整个循环,直到你验证了所有状态。

通过为每种状态编写一个故事,你省去了第二个步骤。你可以直接从编辑代码转到验证所有测试用例。因此,大大加快了整个过程。

编写故事还能揭示出你如果采用更随意的方式开发就不会考虑到的场景。例如,如果用户输入一个很长的任务,会发生什么?让我们添加一个故事来找出答案。

const longTitleString = `This task's name is absurdly large. In fact, I think if I keep going I might end up with content overflow. What will happen? The star that represents a pinned task could have text overlapping. The text could cut-off abruptly when it reaches the star. I hope not!`;

export const LongTitle = Template.bind({});
LongTitle.args = {
  task: {
    id: '4',
    title: longTitleString,
    state: 'TASK_INBOX',
  },
};

4. 自动捕获回归问题

Task 组件在所有用例中都符合我们的预期。但是,我们如何确保未来不会因为一处无关紧要的 CSS 代码导致它崩溃?在每次进行更改时都手动检查整个组件目录是不现实的。

这就是为什么开发者使用视觉回归测试工具来自动检查回归问题的原因。Auth0、Twilio、Adobe 和 Peloton 都使用了 Storybook 团队开发的 Chromatic

此时,我们知道组件处于良好状态。Chromatic 将捕获每个故事的图像快照——就像它在浏览器中显示的那样。然后,当你进行任何更改时,会捕获一个新的快照,并与之前的快照进行比较。然后,你审查发现的任何视觉差异,以决定它们是预期的更新还是意外的错误。

设置 Chromatic

登录并创建一个新项目并获取你的项目令牌 (project-token)。

Chromatic 专为 Storybook 构建,无需配置。运行以下命令将触发它捕获每个故事的快照(使用云端浏览器)。

npx chromatic --project-token=<project-token>

第一次运行将设置为基线(baseline),即起点。每个故事都有自己的基线。

运行测试

在每次提交(commit)时,都会捕获新的快照并与现有基线进行比较,以检测 UI 变化。让我们看看这个检查是如何工作的。

首先,对 UI 进行一些调整。我们将更改置顶图标和文本样式。更新 Task 组件,然后提交代码并再次运行 Chromatic。

// src/components/Task.js
import React from 'react';
import PropTypes from 'prop-types';
import {
  Checkbox,
  Flex,
  IconButton,
  Input,
  Box,
  VisuallyHidden,
} from '@chakra-ui/react';
import { StarIcon } from '@chakra-ui/icons';

export const Task = ({
  task: { id, title, state },
  onArchiveTask,
  onTogglePinTask,
  onEditTitle,
  ...props
}) => (
  <Flex
    as="li"
    _notLast={{
      borderBottom: '1px',
      borderColor: 'gray.200',
    }}
    h={12}
    bg="white"
    alignItems="center"
    _hover={{
      bgGradient: 'linear(to-b,  brand.100,  brand.50)',
    }}
    aria-label={title}
    tabIndex="0"
    {...props}
  >
    <Checkbox
      px={4}
      isChecked={state === 'TASK_ARCHIVED'}
      onChange={(e) => onArchiveTask(e.target.checked, id)}
    >
      <VisuallyHidden>Archive</VisuallyHidden>
    </Checkbox>
    <Box width="full" as="label">
      <VisuallyHidden>Edit</VisuallyHidden>
      <Input
        variant="unstyled"
        flex="1 1 auto"
        color={state === 'TASK_ARCHIVED' ? 'gray.400' : 'gray.700'}
        textDecoration={state === 'TASK_ARCHIVED' ? 'line-through' : 'none'}
        fontSize="sm"
        isTruncated
        value={title}
        onChange={(e) => onEditTitle(e.target.value, id)}
      />
    </Box>
    <IconButton
      p={5}
      flex="none"
      aria-label={state === 'TASK_PINNED' ? 'unpin' : 'pin'}
      variant={state === 'TASK_PINNED' ? 'unpin' : 'pin'}
      icon={<StarIcon />}
      onClick={() => onTogglePinTask(state, id)}
    />
  </Flex>
);

Task.propTypes = {
  task: PropTypes.shape({
    id: PropTypes.string.isRequired,
    title: PropTypes.string.isRequired,
    state: PropTypes.string.isRequired,
  }),
  onArchiveTask: PropTypes.func.isRequired,
  onTogglePinTask: PropTypes.func.isRequired,
  onEditTitle: PropTypes.func.isRequired,
};

现在你将看到一个差异(diff)。

回归测试确保我们不会意外引入更改。但这仍然取决于你来决定更改是有意的还是无意的。

✅ 如果更改是有意的,点击接受(accept)。新的快照将成为基线。

❌ 如果更改是无意的,点击拒绝(deny)。构建将失败。修复代码并再次运行 Chromatic。

在我们的例子中,更改是有意的。请继续点击接受所有故事。

下 面展示了整个工作流程。该手册更详细地介绍了如何使用 Github Actions 在每次提交时触发此工作流程。

结论

视觉错误很容易引入——泄露的 CSS 或一个损坏的组件可能会引发多个问题。它们尤其令人沮丧,并且很少被常用的测试方法捕获。

视觉测试提供了很高的价值,而且维护所需的工作量非常少。与其他形式的测试不同,它们评估组件实际渲染的输出。因此,你可以确保 UI 的外观符合预期——在你第一次构建它时。并且在后续更新过程中,不会无意中引入更改。如需更深入地了解此主题,请阅读完整指南:视觉测试手册

如何真正测试 UI

外观只是 UI 需要测试的众多 众多特征 之一。在下一篇文章中,我将在此概念的基础上介绍组合测试(composition testing)。你将学习如何验证多个组件是否作为一个系统协同工作。加入邮件列表以获取更多 UI 测试文章的通知。

加入 Storybook 邮件列表

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

7,180开发者及仍在增加

我们正在招聘!

加入 Storybook 和 Chromatic 背后的团队。构建被数十万开发者用于生产环境的工具。优先考虑远程工作。

查看职位

热门文章

必备布局调试

让布局调试变得轻而易举的 Storybook 插件
loading
Varun Vachhar

适用于 Angular 12 的 Storybook

支持下一代渲染管道
loading
Michael Shilman

适用于 CSS 的 Storybook 插件

更好的组件样式工作流程
loading
Varun Vachhar
加入社区
7,180开发者及仍在增加
原因为什么选择 Storybook组件驱动 UI
文档指南教程更新日志遥测
社区插件参与其中博客
案例展示探索项目组件术语表
开源软件
Storybook - Storybook 中文

特别鸣谢 Netlify CircleCI