返回博客

Storybook 中的可视化测试

了解如何自动精确定位 UI 错误

loading
Varun Vachhar
@winkerVSbecks
最近更新

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

人们对可视化测试日益增长的兴趣是由于交付无错误 UI 的难度所驱动的。过去,开发者使用单元测试和快照测试来扫描 HTML 代码块中的错误。但是这些方法无法表示用户实际看到的内容,因此错误始终存在。

可视化测试通过捕获和比较真实浏览器中的图像快照来捕获错误。它允许您自动化检查 UI 外观是否正确的过程。

可视化测试手册是一份深入指南,介绍了 UI 外观测试——其中包含来自 BBC、Adobe、Target 等领先工程团队的学习经验。本文总结了该指南中概述的基本工具和技术。

什么是可视化错误?

可视化错误无处不在。元素被截断。颜色或字体样式不正确。布局错乱。以及缺少错误状态。

每家公司现在都是一家软件公司。这意味着每家公司都有责任维护 UI。但如果您像我一样,您可能已经注意到,公司似乎永远没有足够的人员来始终监控 UI 的每个部分。

可视化错误是 UI 外观中无意的错误,使其看起来不可信。它们是易于用肉眼发现但常见测试方法无法捕获的回归。

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

嗯,这个问题有两层。

1. 它看起来对吗?

例如,以这个 Task 组件为例。它的外观根据其所处的状态而有所不同。我们显示一个选中(或未选中)的复选框、一些关于任务的信息以及一个固定按钮。当然,还有所有相关的样式。

第一个挑战只是验证组件在所有这些场景中的外观。这需要大量调整 props 和状态来设置和测试每种情况。哦,而且计算机无法真正告诉您它是否符合规范。您,开发者本人,必须目视检查它。

2. 它仍然看起来对吗?

您第一次构建它是正确的。它在所有状态下看起来都不错。但是,更改发生在开发的自然过程中。错误不可避免地会潜入。对于界面来说尤其如此。一个小的 CSS 调整可能会破坏一个组件或其状态之一。

您不能每次进行更改时都手动检查 UI 的广度。您需要更自动化的东西。

可视化测试

可视化测试允许您使用一个统一的工作流程来处理这两项任务。它是验证您正在构建的组件外观的过程。并在您迭代以交付新功能时再次验证。

以下是可视化测试工作流程的样子

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

可视化测试的关键是将 UI 与应用程序的其余部分(数据、后端、API)隔离。这允许您单独观察每个状态。然后,您可以手动抽查并自动回归测试这些状态。

教程

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

在此处获取代码以跟随操作: https://github.com/chromaui/ui-testing-guide-code

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

1. 隔离组件

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

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

  • 📥 一个沙箱,用于隔离渲染每个组件
  • 🔭 将其所有状态可视化为 stories
  • 📑 为每个组件 记录 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。一个 story 捕获组件的特定状态——浏览器中的实际渲染状态。

Task 组件有三种状态——默认、固顶和归档。我们将为每种状态添加一个 story。

// 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. 评估其外观

然后重复整个周期,直到您验证了其所有状态。

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

编写 stories 还会浮现出您以更临时方式开发时不会考虑到的场景。例如,如果用户输入一个非常长的任务会发生什么?让我们添加该 story 以找出答案。

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 组件在其所有用例中看起来都符合我们的预期。但是,我们如何确保 stray 一行 CSS 代码在将来不会破坏它?每当您进行更改时,手动浏览组件的整个目录是不现实的。

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

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

设置 Chromatic

登录并创建一个新项目并获取您的项目令牌。

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

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

首次运行将设置为基线,即起点。并且每个 story 都有自己的基线。

运行测试

在每次提交时,都会捕获新快照并与现有基线进行比较,以检测 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,
};

您现在将看到差异。

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

✅ 如果更改是有意的,请按接受。新快照现在将设置为基线。

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

在我们的例子中,更改是有意的。继续并单击接受所有 stories。

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

结论

可视化错误很容易引入——泄漏的 CSS 或一个损坏的组件会级联成多个问题。它们尤其令人沮丧地难以调试,并且很少被常见的测试方法捕获。

可视化测试提供高价值,并且需要很少的维护工作。与其他形式的测试不同,它们评估组件的实际渲染输出。因此,您可以确保 UI 看起来符合预期——当您第一次构建它时。并且在后续更新期间不会偷偷溜过任何意外的更改。要更深入地探讨这个主题,请阅读完整指南: 可视化测试手册

如何真正测试 UI

外观只是要测试的 UI 的 众多特性之一。在下一篇文章中,我将基于这些概念来研究组合测试。您将学习如何验证多个组件是否作为一个系统协同工作。加入邮件列表以在发布更多 UI 测试文章时获得通知。

加入 Storybook 邮件列表

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

6,730开发者及更多

我们正在招聘!

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

查看职位

热门文章

必要的布局调试

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

Angular 12 的 Storybook

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

CSS 的 Storybook 插件

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

特别感谢 Netlify CircleCI