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

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

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


获取代码并通过运行 yarn start 在开发模式下加载应用程序。然后打开 https://:3000,您将看到登录屏幕。
设置 Cypress
运行:yarn add cypress --dev 以安装 Cypress 包。然后将 Cypress 命令添加到 package.json 文件的 scripts 字段中。
"scripts": {
"cypress": "cypress open"
}接下来,在项目根目录中添加一个 cypress.json 文件。在这里,我们可以配置应用程序的基础 URL,这样在编写实际测试命令时就不必重复了。
// cypress.json
{
"baseUrl": "https://: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 可以轻松地在组件级别上测试它们。您将用例捕获为故事,并使用 Jest 和 Testing Library 执行断言。这些测试易于维护,并提供快速反馈循环。但是,它们不允许您测试跨越多个页面的流程。
另一方面,E2E 测试在整个应用程序实例上运行。您可以测试用户流程并验证系统所有层是否按预期工作。缺点是该过程需要大量时间和精力。
考虑到这种权衡,大多数团队将自己限制在一小组 E2E 测试中,用于核心用户流程。并使用组件级测试进行更广泛的覆盖。
测试只有在您持续运行它们时才有用。领先的工程团队使用持续集成 (CI) 服务器来运行其完整的测试套件——在每次代码推送时自动运行。下一篇文章将回顾完整的 UI 测试策略,并向您展示如何自动化该工作流程。加入邮件列表,以便在发布更多 UI 测试文章时收到通知。
是组件错误、事件失灵、应用状态问题,还是可能是损坏的 API?
— Storybook (@storybookjs) 2021年8月18日
在生产环境中调试简直是场噩梦!
E2E 测试通过测试您系统的所有层来捕获集成问题。了解如何使用它们以及应避免哪些陷阱。https://#/lR0nq1qjTZ pic.twitter.com/lIJPoGiRiO