
测试用户流程
验证您的 UI 端到端工作

在生产环境中调试是一场噩梦。您必须检查应用程序的每一层。是组件错误、事件错误触发、样式问题、应用程序状态问题,还是 API 损坏?可能是以上任何一种情况,您都必须解开原因。
UI 帮助人们在多个页面上导航一系列步骤以完成他们的目标。 Storybook 使隔离每个页面并对其运行可视化、可访问性和交互测试变得容易。但是要验证整个流程并捕获集成问题,您需要端到端 (E2E) UI 测试。
我研究了 Peloton、Shopify、O'Reilly 等十个领先的前端团队,以了解他们如何应用 E2E 测试来检查用户流程。本文总结了我的发现。您还将了解所涉及的工具以及 E2E 测试如何融入您的 UI 测试策略。
您的 UI 是否端到端工作?
用户流程不只包含一个组件,它们涉及多个组件协同工作。每次交互都会触发状态更新、路由更改和 API 调用,这些都会影响屏幕上呈现的内容。由于存在所有这些故障点,因此逐个 QA 它们可能很困难。
团队使用 E2E 测试来确保用户体验按预期工作。要运行 E2E 测试,您首先要启动应用程序的完整实例。然后使用 Cypress、Playwright 或 Selenium 等工具,通过模拟用户行为来验证用户流程。

测试完整的应用程序会带来权衡
从表面上看,E2E 和交互测试看起来非常相似。但请记住,您的用户除了与其组成组件之外,还会与应用程序进行交互。 E2E 测试在应用程序级别运行,这使它们能够发现前端和后端之间的集成问题。但这还需要您为技术堆栈的更多层维护测试基础设施(耗时!)。
组件级测试由可以挂载、渲染和测试组件的独立工具完成。对于 E2E 测试,您负责启动应用程序。为此,您有两个选择
- 维护完整的测试环境:这包括前端、后端、服务和种子测试数据。例如,O'Reilly 团队使用 Docker 来启动他们的整个应用程序基础设施并运行 E2E 测试。
- 维护仅前端的测试环境,并配有模拟后端。例如,Twilio 通过使用 Cypress 存根网络请求来测试流程。
无论哪种方式,复杂性都会随着系统规模的扩大而增加。系统越大,在持续集成服务器上复制设置,然后连接到云浏览器来运行测试就越麻烦。
鉴于这种权衡,大多数团队使用混合方法来平衡工作量和价值。 E2E 测试仅限于关键用户流程。交互测试用于验证所有其他行为。
在本教程中,我们将使用 Cypress 和模拟后端方法进行 E2E 测试。以下是工作流程的摘要
- ⚙️ 设置:启动应用程序并模拟网络请求(重用来自 stories 的数据)
- 🤖 操作:使用 Cypress 访问页面并模拟交互
- ✅ 运行断言以验证 UI 是否已正确更新
教程
让我们看看在之前的文章中介绍的 Taskbox 应用程序的工作流程操作。我们将为身份验证流程编写 E2E 测试:导航到登录页面并填写用户凭据。身份验证后,用户应该能够看到他们的任务列表。


获取代码并通过运行 yarn start
在开发模式下加载应用程序。然后打开https://127.0.0.1:3000,您将看到登录屏幕。
设置 Cypress
运行:yarn add cypress --dev
以安装 Cypress 包。然后将 Cypress 命令添加到您的 package.json
文件的 scripts 字段。
"scripts": {
"cypress": "cypress open"
}
接下来,在项目的根目录添加一个 cypress.json
文件。在这里,我们可以为我们的应用程序配置基本 URL,这样我们在编写实际的测试命令时就不必重复自己。
// cypress.json
{
"baseUrl": "https://127.0.0.1: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 测试文章发布时的通知。
是组件错误、事件错误触发、应用程序状态问题,还是 API 损坏?
— Storybook (@storybookjs) 2021 年 8 月 18 日
在生产环境中调试是一场噩梦!
E2E 测试通过测试系统的所有层来捕获集成问题。了解如何使用它们以及避免哪些陷阱。https://127.0.0.1/lR0nq1qjTZ pic.twitter.com/lIJPoGiRiO