返回博客

测试用户流程

验证你的 UI 是否端到端工作

loading
Varun Vachhar
@winkerVSbecks
最后更新于

在生产环境中调试是一个噩梦。你必须检查应用的每一层。是组件错误、事件触发异常、样式问题、应用状态问题,还是 API 调用失败?可能是上述任何一个问题,而你必须弄清楚原因。

UI 帮助用户在多个页面上按照一系列步骤达成目标。Storybook 使得隔离这些页面并对其运行可视化可访问性交互测试变得容易。但要验证整个流程并捕获集成问题,你需要端到端 (E2E) UI 测试。

我研究了十家领先的前端团队——Peloton、Shopify、O'Reilly 等——以了解他们如何应用 E2E 测试来检查用户流程。本文总结了我的发现。你还将了解相关的工具以及 E2E 测试如何融入你的 UI 测试策略。

你的 UI 是否端到端工作?

用户流程不仅仅局限于一个组件,它们涉及多个组件协同工作。每次交互都会触发状态更新、路由变更和 API 调用,这些都会影响屏幕上渲染的内容。有如此多的潜在故障点,一一进行质量检查可能会很困难。

团队使用 E2E 测试来确保用户体验按预期工作。要运行 E2E 测试,首先需要启动一个完整的应用实例。然后使用 Cypress、Playwright 或 Selenium 等工具模拟用户行为来验证用户流程。

应用通过将组件连接到数据、业务逻辑和 API 来组装

测试完整的应用需要权衡取舍

表面上看,E2E 测试和交互测试非常相似。但请记住,用户与应用互动,而不仅仅是其组成组件。E2E 测试在应用层面运行,这使得它们能够发现前端和后端之间的集成问题。但这同时也要求你维护技术栈更多层的测试基础设施(非常耗时!)。

组件级测试是由独立的工具完成的,这些工具可以挂载、渲染和测试组件。对于 E2E 测试,你需要负责启动应用。你有两种选择

  1. 维护一个完整的测试环境:这包括前端、后端、服务和预设的测试数据。例如,O'Reilly 团队使用 Docker 启动整个应用基础设施并运行 E2E 测试。
  2. 维护一个仅包含前端的测试环境,搭配模拟后端例如,Twilio 使用 Cypress 模拟网络请求来测试流程。

无论哪种方式,复杂性都会随着系统规模的扩大而增加。系统越大,在持续集成服务器上复制设置然后连接到云浏览器运行测试就越麻烦。

考虑到这种权衡,大多数团队采用混合方法来平衡投入和价值。E2E 测试仅限于关键的用户流程。而交互测试则用于验证所有其他行为。

在本教程中,我们将使用 Cypress 并采用模拟后端的方法进行 E2E 测试。以下是工作流程总结

  1. ⚙️ 设置:启动应用并模拟网络请求(重用 story 中的数据)
  2. 🤖 操作:使用 Cypress 访问页面并模拟交互
  3. 运行断言以验证 UI 是否正确更新

教程

让我们通过我在之前的一篇文章中介绍的 Taskbox 应用来看看工作流程实际操作。我们将为认证流程编写一个 E2E 测试:导航到登录页面并填写用户凭据。认证成功后,用户应该能够看到他们的任务列表。

获取代码并通过运行 yarn start 在开发模式下加载应用。然后打开 http://localhost:3000,你将看到登录屏幕。

设置 Cypress

运行:yarn add cypress --dev 安装 Cypress 包。然后将 Cypress 命令添加到你的 package.json 文件的 scripts 字段中。

 "scripts": {
   "cypress": "cypress open"
 }

接下来,在你的项目根目录添加一个 cypress.json 文件。在这里我们可以配置应用的基础 URL,这样在编写实际的测试命令时就不必重复输入。

// cypress.json
{
 "baseUrl": "http://localhost:3000"
}

最后,运行 yarn cypress 完成设置过程。这将在你的项目中添加一个 cypress 文件夹。所有测试文件都将放在这里。它还会启动 Cypress 测试运行器。

测试认证流程

Cypress 的测试结构与你可能熟悉的其它类型的测试非常相似。你首先描述要测试什么。每个测试都位于一个 it 块中,你在这里运行断言。以下是认证用户流程测试的代码示例

// cypress/e2e/auth.spec.js

describe('The Login Page', () => {
  it('user can authenticate using the login form', () => {
    const email = 'alice.carr@test.com';
    const password = 'k12h1k0$5;lpa@Afn';

    cy.visit('/');

    // Fill out the form
    cy.get('input[name=email]').type(email);
    cy.get('input[name=password]').type(`${password}`);

    // Click the sign-in button
    cy.get('button[type=submit]').click();

    // UI should display the user's task list
    cy.get('[aria-label="tasks"] li').should('have.length', 6);
  });
});

让我们分解一下这里发生了什么。cy.visit 会在浏览器中打开我们应用的登录页面。然后我们使用 cy.get 命令查找并填写邮箱和密码字段。最后,点击提交按钮实际进行登录。

测试的最后一部分运行断言。换句话说,我们验证认证是否成功。我们通过检查任务列表是否可见来实现这一点。

切换到 Cypress 窗口,你应该能看到测试正在执行。

但是,请注意测试失败了。那是因为我们只运行了应用的前端。所有 HTTP 请求都会失败,因为我们没有活动的后端。与其启动实际的后端,不如使用模拟的网络请求。

模拟请求

cy.intercept 方法允许我们拦截网络请求并使用模拟数据进行响应。认证用户流程依赖于两个请求:/authenticate 用于登录,以及 /tasks 用于获取用户的任务。为了模拟这些请求,我们需要一些模拟数据。

之前的一篇文章中,我展示了如何使用 Storybook 目录化一个组件的所有用例。在该过程中,我们在 TaskList.stories.js 文件中为任务列表创建了模拟数据。我们可以在 Cypress 测试中重用这些数据。

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

export default {
  component: TaskList,
  title: 'TaskList',
  argTypes: {
    ...Task.argTypes,
  },
};
const Template = (args) => <TaskList {...args} />;

export const Default = Template.bind({});
Default.args = {
  tasks: [
    { id: '1', state: 'TASK_INBOX', title: 'Build a date picker' },
    { id: '2', state: 'TASK_INBOX', title: 'QA dropdown' },
    {
      id: '3',
      state: 'TASK_INBOX',
      title: 'Write a schema for account avatar component',
    },
    { id: '4', state: 'TASK_INBOX', title: 'Export logo' },
    { id: '5', state: 'TASK_INBOX', title: 'Fix bug in input error state' },
    { id: '6', state: 'TASK_INBOX', title: 'Draft monthly blog to customers' },
  ],
};

让我们继续更新测试以模拟这两个网络请求。

// cypress/e2e/auth.spec.js

import { Default as TaskListDefault } from '../../src/components/TaskList.stories';

describe('The Login Page', () => {
  beforeEach(() => {
    cy.intercept('POST', '/authenticate', {
      statusCode: 201,
      body: {
        user: {
          name: 'Alice Carr',
          token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
        },
      },
    });

    cy.intercept('GET', '/tasks', {
      statusCode: 201,
      body: TaskListDefault.args,
    });
  });

  it('user can authenticate using the login form', () => {
    const email = 'alice.carr@test.com';
    const password = 'k12h1k0$5;lpa@Afn';

    cy.visit('/');

    // Fill out the form
    cy.get('input[name=email]').type(email);
    cy.get('input[name=password]').type(`${password}`);

    // Click the sign-in button
    cy.get('button[type=submit]').click();

    // UI should display the user's task list
    cy.get('[aria-label="tasks"] li').should('have.length', 6);
  });
});

重新运行测试,现在应该会通过了。

我们启动了完整的应用。使用 Cypress,我们能够模拟用户行为并测试登录流程。在这个测试中,我们检查了数据流、表单提交和 API 调用。

结论

用户体验是通过组合组件并将它们连接到数据和 API 来构建的。Storybook 使得在组件级别测试它们变得容易。你将用例捕获为 stories,并使用 Jest 和 Testing Library 执行断言。这些测试易于维护并提供快速反馈。然而,它们不允许你测试跨多个页面的流程。

另一方面,E2E 测试在应用的完整实例上运行。你可以测试用户流程并验证系统的所有层是否按预期工作。缺点是这个过程需要大量时间和精力。

考虑到这种权衡,大多数团队只为核心用户流程编写少量 E2E 测试。并使用组件级测试来获得更广泛的覆盖。

测试只有持续运行才有帮助。领先的工程团队使用持续集成 (CI) 服务器来运行完整的测试套件——在每次代码提交时自动运行。下一篇文章将回顾完整的 UI 测试策略,并展示如何自动化该工作流程。加入邮件列表以获取更多 UI 测试文章发布通知。

加入 Storybook 邮件列表

获取最新新闻、更新和发布信息

7,180名开发者及以上

我们正在招聘!

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

查看职位

热门文章

如何使用 Github Actions 自动化 UI 测试

加速你的工作流程并交付更高质量的代码
loading
Varun Vachhar

UI 测试指南

不会减慢你的测试工作流程
loading
Varun Vachhar

交互测试先睹为快

使用 Storybook 的 play 函数测试连接的组件
loading
Dominic Nguyen
加入社区
7,180名开发者及以上
原因为何选择 Storybook组件驱动的 UI
文档指南教程变更日志遥测
社区插件参与贡献博客
案例展示探索项目组件词汇表
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI