返回UI 测试手册
React
章节
  • 简介
  • 视觉
  • 组合
  • 交互
  • 可访问性
  • 用户流程
  • 自动化
  • 工作流
  • 结论

Storybook 中的视觉测试

了解如何自动精确定位 UI bug
此社区翻译尚未更新到最新的 Storybook 版本。请将英文指南中的更改应用到此翻译中,帮助我们更新它。 欢迎提交 Pull Request.

要发布无 bug 的 UI 是很难的。过去,开发人员使用单元测试和快照测试来扫描 HTML 块中的 bug。但这些方法不能反映用户实际看到的内容,因此 bug 始终存在。

视觉测试通过在真实浏览器中捕获和比较图像快照来发现 bug。它允许您自动化检查 UI 是否看起来正确的流程。

什么是视觉 bug?

视觉 bug 无处不在。截断的元素。错误的颜色或字体样式。损坏的布局。以及缺失的错误状态。

如今,每家公司都是一家软件公司。这意味着每家公司都负责维护 UI。但如果你和我一样,你可能会注意到,公司似乎总是不够人手来时刻监控 UI 的每一个部分。

视觉 bug 是 UI 外观中无意发生的错误,这些错误会让 UI 看起来不可靠。它们很容易凭肉眼发现,但常见的测试方法却无法捕获。

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

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

1. 看起来正确吗?

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

Different states of a task component

第一个挑战只是验证组件在所有这些场景中的外观。这需要大量调整 props 和 state 来设置和测试每种情况。哦,而且计算机真的不能告诉你它是否符合规范。你,开发人员,必须对其进行视觉检查。

2. 它仍然看起来正确吗?

你第一次就把它做对了。它在所有状态下看起来都很好。但随着开发的自然进行,会发生变化。bug 几乎不可避免地会潜入。特别是对于界面。一个小的 CSS 调整可能会破坏一个组件或其某个状态。

每次更改代码时,你都无法手动检查 UI 的全部范围。你需要更自动化的东西。

视觉测试

视觉测试允许您通过一个统一的工作流程来解决这两个任务。它是指在构建组件时验证其外观的过程。并且在迭代以发布新功能时再次进行。

视觉测试工作流程是这样的:

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

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

让我们详细了解每一步。

1. 隔离组件

一次测试一个组件并为每种状态编写测试用例,这样更容易发现 bug。传统的方法是在组件首次使用的应用程序页面上构建组件。这使得模拟和验证所有这些状态变得困难。有更好的方法——Storybook。

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

  • 📥 一个用于独立渲染每个组件的沙箱
  • 🔭 将所有状态可视化为故事
  • 📑 为每个组件记录 props 和使用指南
  • 🗃️ 一个包含所有组件的目录,便于发现

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

设置 Storybook

我们的项目已预先配置为使用 Storybook。配置位于 .storybook 文件夹中,所有必要的脚本都已添加到 package.json 中。

我们可以通过为 Task 组件创建一个 story 文件来开始。这会将组件注册到 Storybook,并添加一个默认的测试用例。

复制
src/components/Task.stories.jsx
import Task from './Task';

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

export const 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.jsx
import Task from './Task';

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

export const Default = {
  args: {
    task: {
      id: '1',
      title: 'Buy milk',
      state: 'TASK_INBOX',
    },
  },
};

export const Pinned = {
  args: {
    task: {
      id: '2',
      title: 'QA dropdown',
      state: 'TASK_PINNED',
    },
  },
};

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

3. 验证

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

通常的开发工作流程是:

  1. 编辑代码
  2. 让组件进入适当的状态
  3. 评估其外观

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

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

编写 stories 还会浮现您在以更随意的 T.方式开发时可能未曾考虑到的场景。例如,如果用户输入了一个非常长的任务,会发生什么?让我们添加那个 story 来找出答案。

复制
src/components/Task.stories.jsx
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 = {
  args: {
    task: {
      id: '4',
      title: longTitleString,
      state: 'TASK_INBOX',
    },
  },
};

现在我们已经验证了每种测试用例的外观,我们可以进入下一步。自动捕获回归。但首先,提交您的更改。

4. 自动捕获回归

Task 组件在其所有用例中看起来都符合我们的预期。但是,我们如何确保一行 CSS 不会在将来破坏它?每次进行更改时,手动遍历整个组件目录是不现实的。

这就是为什么开发人员使用视觉回归测试工具来自动检查回归。Auth0、Twilio、Adobe 和 Peloton 使用 Chromatic(由 Storybook 团队构建)。

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

设置 Chromatic

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

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

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

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

运行测试

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

首先,对 UI 进行一些 T.。我们将更改图钉图标和文本样式。更新 Task 组件,然后进行 T.并重新运行 Chromatic。

复制
src/components/Task.jsx
import PropTypes from 'prop-types';

export default function Task({
  task: { id, title, state },
  onArchiveTask,
  onTogglePinTask,
  onEditTitle,
}) {
  return (
    <div
      className={`list-item ${state}`}
      role="listitem"
      aria-label={`task-${id}`}
    >
      <label
        htmlFor="checked"
        aria-label={`archiveTask-${id}`}
        className="checkbox"
      >
        <input
          type="checkbox"
          disabled={true}
          name="checked"
          id={`archiveTask-${id}`}
          checked={state === "TASK_ARCHIVED"}
        />
        <span
          className="checkbox-custom"
          onClick={() => onArchiveTask("ARCHIVE_TASK", id)}
          role="button"
          aria-label={`archiveButton-${id}`}
        />
      </label>

      <label htmlFor="title" aria-label={title} className="title">
        <input
          type="text"
          value={title}
          name="title"
          placeholder="Input title"
          style={{ textOverflow: "ellipsis" }}
          onChange={(e) => onEditTitle(e.target.value, id)}
        />
      </label>

      {state !== "TASK_ARCHIVED" && (
        <button
          className="pin-button"
          onClick={() => onTogglePinTask(state, id)}
          id={`pinTask-${id}`}
          aria-label={state === "TASK_PINNED" ? "unpin" : "pin"}
          key={`pinTask-${id}`}
        >
+         <span className={`icon-star`} />
        </button>
      )}
    </div>
  );
}

Task.propTypes = {
  /** Composition of the task */
  task: PropTypes.shape({
    /** Id of the task */
    id: PropTypes.string.isRequired,
    /** Title of the task */
    title: PropTypes.string.isRequired,
    /** Current state of the task */
    state: PropTypes.string.isRequired,
  }),
  /** Event to change the task to archived */
  onArchiveTask: PropTypes.func.isRequired,
  /** Event to change the task to pinned */
  onTogglePinTask: PropTypes.func.isRequired,
  /** Event to change the task title */
  onEditTitle: PropTypes.func.isRequired,
};

您现在将看到一个 diff。

回归测试确保我们不会意外引入更改。但最终还是由您决定这些更改是否是故意的。

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

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

在我们的例子中,这些更改是故意的。请继续为所有 stories 点击“接受”。整个工作流程如下所示。

Build in storybook and run visual tests with Chromatic. If changes look good, then merge your PR.

防止一个 bug 演变成多个 bug

一点点泄露的 CSS 或一个损坏的组件可能会滚雪球般地导致多个问题。这些 bug 特别难以调试。在下一章中,我们将在此基础上学习如何捕获此类级联问题。

将您的代码与本章保持同步。在 GitHub 上查看 6a337af。
这个免费指南对您有帮助吗?在 Twitter 上分享以表达您的赞赏,并帮助其他开发人员找到它。
下一章
组合
防止小改动变成大回归
✍️ 在 GitHub 上编辑 – PR 欢迎!
加入社区
7,424开发者及更多
原因为什么选择 Storybook组件驱动的 UI
文档指南教程更新日志遥测
社区插件参与进来博客
展示探索关于
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI