返回博客

使用 Storybook 测试组件交互

关于如何模拟和验证用户行为的完整教程

loading
Varun Vachhar
@winkerVSbecks
最后更新于

组件负责获取数据、响应用户交互并管理应用状态。为了验证这种功能行为,开发者依赖自动化测试。但大多数测试工具都基于 Node 和 JSDOM。这意味着你不得不通过 文本 命令行来调试 视觉 UI。

在 Storybook 中,我们正通过使用浏览器运行测试来改进组件测试。在过去的六个月里,我们引入了 几项功能——play 函数、test-runner、断言库——以实现这一目标。本文将详细介绍整个 Storybook 交互测试工作流程。

  • 📝 在 stories 文件中编写测试
  • 🐛 使用交互面板在浏览器中调试测试
  • 🔗 通过 URL 复现错误状态
  • 🤖 使用持续集成自动化测试

Storybook 中的组件测试是如何工作的?

测试交互是验证用户行为的常用模式。你提供模拟数据来设置测试场景,使用 Testing Library 模拟用户交互,并检查最终的 DOM 结构。

在 Storybook 中,这一熟悉的工作流程在你的浏览器中进行。这使得调试失败变得更容易,因为你在与组件开发相同的环境(浏览器)中运行测试。

首先,编写一个 story 来设置组件的初始状态。然后,使用 play 函数 模拟用户行为,例如点击和表单输入。最后,使用 Storybook test-runner 来检查 UI 和组件状态是否正确更新。通过命令行或你的 CI 服务器实现测试自动化。

教程

为了演示测试工作流程,我将使用 Taskbox 应用——一个类似于 Asana 的任务管理应用。在它的 InboxScreen 上,用户可以点击星形图标来置顶任务。或者点击复选框来归档任务。我们来编写测试,以确保 UI 正确响应这些交互。

获取代码进行学习

# Clone the template
npx degit chromaui/ui-testing-guide-code#dc9bacae842f5250aad544b139dc9d63a48bbd1e taskbox

cd taskbox

# Install dependencies
yarn

设置 test-runner

我们将从安装 test-runner 和相关包开始(注意,它需要 Storybook 6.4 或更高版本)。

yarn add -D @storybook/testing-library @storybook/jest @storybook/addon-interactions jest @storybook/test-runner

更新你的 Storybook 配置(在 .storybook/main.js 中),以包含 interactions 插件并启用用于调试的播放控制。

 // .storybook/main.js
 
module.exports = {
 stories: [],
 addons: [
   // Other Storybook addons
   '@storybook/addon-interactions', // 👈 addon is registered here
 ],
 features: {
   interactionsDebugger: true, // 👈 enable playback controls
 },
};

然后将测试任务添加到你的项目 package.json 文件中

{
 "scripts": {
   "test-storybook": "test-storybook"
 }
}

最后,启动你的 Storybook(test-runner 针对正在运行的 Storybook 实例执行)

yarn storybook

编写 stories 来设置测试用例

编写测试的第一步是通过向组件提供 props 或模拟数据来设置场景。这正是 story 的作用,所以我们来为 InboxScreen 组件编写一个 story。

InboxScreen 通过 /tasks API 请求获取数据,我们将使用 MSW 插件 来模拟它。

// src/InboxScreen.stories.js;
 
import React from 'react';
import { rest } from 'msw';
import { within, userEvent, findByRole } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
import { InboxScreen } from './InboxScreen';
import { Default as TaskListDefault } from './components/TaskList.stories';
 
export default {
 component: InboxScreen,
 title: 'InboxScreen',
};
 
const Template = (args) => <InboxScreen {...args} />;
 
export const Default = Template.bind({});
Default.parameters = {
 msw: {
   handlers: [
     rest.get('/tasks', (req, res, ctx) => {
       return res(ctx.json(TaskListDefault.args));
     }),
   ],
 },
};

使用 play 函数编写交互测试

Testing Library 提供了一个方便的 API 用于模拟用户交互——点击、拖拽、轻触、输入等。Jest 则提供断言工具。我们将使用这两个工具的 Storybook 封装版本来编写测试。因此,你将获得一个熟悉的、对开发者友好的语法来与 DOM 交互,同时还有额外的遥测数据帮助调试。

测试本身将包含在一个 play 函数 中。这段代码片段会附加到 story 上,并在 story 渲染后运行。

让我们加入第一个交互测试,以验证用户可以置顶任务

export const PinTask = Template.bind({});
PinTask.parameters = { ...Default.parameters };
PinTask.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
  const getTask = (name) => canvas.findByRole('listitem', { name });

  // Find the task to pin
  const itemToPin = await getTask('Export logo');
  
  // Find the pin button
  const pinButton = await findByRole(itemToPin, 'button', { name: 'pin' });
  
  // Click the pin button
  await userEvent.click(pinButton);
  
  // Check that the pin button is now a unpin button
  const unpinButton = within(itemToPin).getByRole('button', { name: 'unpin' });
  await expect(unpinButton).toBeInTheDocument();
};

每个 play 函数都接收 Canvas 元素——story 的顶层容器。你可以将查询范围限制在此元素内,从而更容易找到 DOM 节点。

在我们的例子中,我们正在寻找“Export logo”任务。然后找到其中的置顶按钮并点击它。最后,我们检查按钮是否已更新为未置顶状态。

当 Storybook 完成 story 渲染后,它会执行 play 函数中定义的步骤,与组件交互并置顶一个任务——就像用户会做的那样。如果你查看你的 interactions panel,你会看到一步步的流程。它还提供了一套方便的 UI 控件,用于暂停、恢复、快退和逐步执行每个交互。

使用 test-runner 执行测试

现在我们已经完成了第一个测试,我们还将为归档、编辑和删除任务功能添加测试。

export const ArchiveTask = Template.bind({});
ArchiveTask.parameters = { ...Default.parameters };
ArchiveTask.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
  const getTask = (name) => canvas.findByRole('listitem', { name });

  const itemToArchive = await getTask('QA dropdown');
  const archiveCheckbox = await findByRole(itemToArchive, 'checkbox');
  await userEvent.click(archiveCheckbox);

  await expect(archiveCheckbox.checked).toBe(true);
};

export const EditTask = Template.bind({});
EditTask.parameters = { ...Default.parameters };
EditTask.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
  const getTask = (name) => canvas.findByRole('listitem', { name });

  const itemToEdit = await getTask('Fix bug in input error state');
  const taskInput = await findByRole(itemToEdit, 'textbox');

  userEvent.type(taskInput, ' and disabled state');
  await expect(taskInput.value).toBe(
    'Fix bug in input error state and disabled state'
  );
};

export const DeleteTask = Template.bind({});
DeleteTask.parameters = { ...Default.parameters };
DeleteTask.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
  const getTask = (name) => canvas.findByRole('listitem', { name });

  const itemToDelete = await getTask('Build a date picker');
  const deleteButton = await findByRole(itemToDelete, 'button', {
    name: 'delete',
  });
  await userEvent.click(deleteButton);

  expect(canvas.getAllByRole('listitem').length).toBe(5);
};

你现在应该能看到这些场景的 stories。Storybook 只在你查看 story 时运行交互测试。因此,你必须逐个 story 查看才能运行所有检查。

每当进行更改时手动审查整个 Storybook 是不切实际的。Storybook test-runner 自动化了这个过程。它是一个独立的实用工具——由 Playwright 提供支持——它运行你的所有交互测试并捕获损坏的 stories。

启动 test-runner(在单独的终端窗口中):yarn test-storybook --watch。它验证所有 stories 是否无误地渲染,并且所有断言都已通过。

如果测试失败,你会获得一个链接,可以在浏览器中打开失败的 story。

你已经理顺了本地开发工作流程。Storybook 和 test-runner 并行运行,让你可以在隔离环境中构建组件,并一次性测试其底层逻辑。

自动化 Storybook 交互测试

一旦你准备好合并代码,你会希望使用持续集成 (CI) 服务器自动运行所有检查。将 Storybook 交互测试集成到测试自动化流水线中有两种选择:在 CI 中使用 test-runner,或将其与 Chromatic 的视觉测试结合使用。

在 CI 中运行 test-runner

你可以在 CI 服务器上构建并提供 Storybook 服务,并对其执行 test-runner。这里是一个使用 concurrentlyhttp-serverwait-on 库的方案。

# .github/workflows/ui-tests.yml
 
name: 'Storybook Tests'
on: push
jobs:
 test:
   timeout-minutes: 60
   runs-on: ubuntu-latest
   steps:
     - uses: actions/checkout@v2
     - uses: actions/setup-node@v2
       with:
         node-version: '14.x'
     - name: Install dependencies
       run: yarn
     - name: Install Playwright
       run: npx playwright install --with-deps
     - name: Build Storybook
       run: yarn build-storybook --quiet
     - name: Serve Storybook and run tests
       run: |
         npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
           "npx http-server storybook-static --port 6006 --silent" \
           "npx wait-on tcp:6006 && yarn test-storybook"

你也可以针对已发布的 Storybook 运行测试。关于这一点以及其他 CI 配置选项的更多信息,请参阅 test-runner 文档

使用 Chromatic 结合交互测试和视觉测试

捕获无意的 UI 更改一直是一个挑战。一行不恰当的 CSS 可能会破坏多个页面。这就是为什么 Auth0、Twilio、Adobe 和 Peloton 等领先团队依赖 视觉测试 的原因。Chromatic 是一个专为 Storybook 构建的基于云的视觉测试工具。它也可以执行你的交互测试。

Chromatic 的工作原理是捕获每个 story 在浏览器中呈现时的图像快照。然后当你发起拉取请求时,它会将其与之前接受的基线进行比较,并向你展示差异。

Chromatic 开箱即用地支持 Storybook 交互测试。它会等待交互测试运行完毕后再捕获快照。这样一来,你可以一次性验证组件的视觉外观和底层逻辑。任何测试失败都会通过 Chromatic UI 进行报告。

这是一个使用 Github Actions 运行 Chromatic 的示例工作流程。对于其他 CI 服务,请参阅 Chromatic 文档

# .github/workflows/ui-tests.yml
 
name: 'Chromatic'
on: push
jobs:
 chromatic-deployment:
   runs-on: ubuntu-latest
   steps:
     - uses: actions/checkout@v2
       with:
         fetch-depth: 0 # Required to retrieve git history
     - name: Install dependencies
       run: yarn
     - name: Publish to Chromatic
       uses: chromaui/action@v1
       with:
         # Grab this from the Chromatic manage page
         projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

使用 Storybook 在浏览器中测试组件

组件不是静态的。用户可以与 UI 交互并触发状态更新。你必须编写模拟用户行为的测试来验证这种行为。

Storybook 交互测试是我们对组件测试的愿景:快速、直观,并与你已使用的工具集成。它结合了实时浏览器直观的调试环境以及无头浏览器的性能和可脚本性。

如果你一直跟着写代码,你的仓库应该看起来像这样:GitHub 仓库

想了解更多?这里有一些额外的有用资源

加入 Storybook 邮件列表

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

7,180开发者,仍在增长

我们正在招聘!

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

查看职位

热门文章

Webpack 的 Storybook 延迟编译

为大型 Storybook 带来闪电般的本地开发体验
loading
Tom Coleman

Storybook 的 Figma 插件

将设计与代码集成,并排查看
loading
Dominic Nguyen

Figma 插件 Beta 版

将 stories 连接到设计组件
loading
Dominic Nguyen
加入社区
7,180开发者,仍在增长
原因为何选择 Storybook组件驱动的 UI
文档指南教程更新日志遥测
社区插件参与贡献博客
展示探索项目组件词汇表
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI