使用 Cypress JS 的现代端到端测试方式





5.00/5 (2投票s)
软件工程中有许多类型的测试:静态测试、单元测试、集成测试、端到端测试、A/B 测试、压力测试、冒烟测试和用户验收测试
为什么要编写测试?
编写测试的最终目标应该是改善用户的应用内体验,并提高开发人员发布新应用或改进应用的信心。
Mattermost 团队一直在持续编写不同类型的测试来改进产品。如此广泛的自动化测试使他们能够在过去几年中每月发布一个新版本——包含新功能和改进。数千名开发人员为代码库做出了贡献。通过自动化测试,Mattermost 的核心团队可以自信地管理、审查和合并这些贡献到产品中。
测试类型
软件工程中有许多类型的测试:静态测试、单元测试、集成测试、端到端测试、A/B 测试、压力测试、冒烟测试和用户验收测试等等。
如果刚开始接触,看到如此庞大的列表可能会有点吓人。更糟糕的是,当一个人谈论某个特定测试时,另一个人可能会对该测试给出不同的定义。
在这篇博文中,我们将在深入探讨端到端测试之前,先简单介绍一下静态测试、单元测试和集成测试。
静态测试
让我们看看我们计算器应用的一小段代码片段
const add = (...operands) => {
let sum = 0;
operands.forEach((operand) => {
sum = sum + operand;
});
return sums;
};
你可能已经注意到了。返回变量应该是 sum
而不是 sums
。虽然这个例子可能很简单,但其他情况——例如访问未知对象的属性错误以及混淆变量——可以通过静态类型检查和 ESLint、Typescript 和 Flow 等 linting 工具及早识别。我们将这些归类为静态测试。
单元测试
现在让我们看一个实际的测试。在这里,我们编写了一个小测试来测试我们之前看到的相同加法函数
import { add } from './add.js';
test('Adding function to return correct sum of numbers', () => {
expect(add(1, 1)).toEqual(2);
expect(add(2, 3)).toEqual(5);
});
如果你不熟悉语法,这完全没关系。我们将用简单的语言解释代码。我们为单个加法函数编写了一套测试。我们用某些数字调用该函数,并期望它返回这些数字的正确总和。尽管我们的应用程序还有其他功能,例如减法和除法,但我们将单独测试它们。
这种隔离测试称为单元测试。然而,一个单元不一定总是函数。它可以是一个类、模块,甚至是对象。
但是,如果我们要串联测试多个这样的单元呢?这就是集成测试发挥作用的地方。
集成测试
在这些类型的测试中,我们验证多个单元是否相互协调工作。与单元测试不同,集成测试考虑了前端应用程序的所有工作部件,并且大多数对后端 API 的调用都被模拟。通过这种设置,覆盖大量应用程序代码所需的精力相对较少,并且测试以相当快的速度运行。
describe('Add page of the app', () => {
it('', () => {
// before the request goes out, we need to set up spying
cy.server();
cy.route({
method: 'POST',
url: '**/add/?operand1=23&operand2=10',
response: {
sum: '33',
},
}).as('getSumOfNumbersAPI');
cy.visit('/add-page');
// # Add the number for first operant
cy.findByLabelText('Number 1').type('23');
// # Add the number for second operand
cy.findByLabelText('Number 2').type('10');
// # Click calculate button
cy.findByText('Calculate').click();
// # Wait for API to get complete
cy.wait('@getSumOfNumbersAPI').should((xhr) => {
expect(xhr.status).to.equal(200);
const { sum } = xhr.response.body;
// * Verify we get the correct sum
cy.findByLabelText('Sum of number').should('have.value', sum);
});
});
});
在上面的例子中,我们假设我们依赖 API 提供两个数字的和,而不是在客户端进行计算。我们没有底层的实现细节,而是依赖于最终结果——这与用户看到和使用应用程序的方式完全相同。
我们找到了我们的输入字段,我们首先输入数字,然后点击“**计算**”按钮。然后我们在测试开始时设置了模拟 API 请求。这样,当请求实际发出时,我们的测试框架将捕获它并返回模拟响应。
最后,验证数字的总和。
端到端测试
与上述所有类型的测试相反,端到端测试运行整个应用程序。这意味着您的客户端、API、数据库和第三方服务都包含在内。
因此,端到端测试给我们提供了非常高的信心,即关键用户流程能够高效工作。它们设置起来有点昂贵,而且速度有点慢,并且还需要大量的初始设置。但是这些测试最接近用户测试应用程序的方式。
describe('Add page of the app', () => {
it('', () => {
cy.visit('/add-page');
// # Add the number for first operant
cy.findByLabelText('Number 1').type('23');
// # Add the number for second operand
cy.findByLabelText('Number 2').type('10');
// # Click calculate button
cy.findByText('Calculate').click();
// # Wait for API to get complete
cy.wait('1000');
// * Verify we get the correct sum
cy.findByLabelText('Sum of number').should('have.value', '33');
});
});
我们可能遇到的下一个问题通常是这样的:我们应该编写多少这样的测试?
嗯,没有一个经验法则规定一个应用程序应该有 *x* 个 A 测试和 *y* 个 B 测试。也没有明确的测试百分比或比率。相反,通过混合使用几种不同类型的测试来增加代码库的信心。
你的测试越接近你的软件被使用的方式,它们就能给你越多的信心。
— Kent C. Dodds 🌌 (@kentcdodds) 2018 年 3 月 23 日
开始端到端测试
有许多工具可以帮助您进行 Web 应用程序的端到端测试。让我们直接深入了解 Cypress,这是一款旨在帮助您编写更快、更简单、更可靠的测试的测试工具。它已经捆绑了许多断言和其他工具。
通过 npm 或 YARN,可以轻松地将 Cypress 添加到现有项目中。
npm install cypress --save-dev
安装后,请务必在 `package.json` 中添加一个脚本,以便轻松访问 Cypress 控制面板。
"scripts": {
"cypress": "cypress open"
...
},
当您第一次使用上述 npm 命令运行 Cypress 时,它将需要一段时间来安装一些先决条件。接下来,您将看到 Cypress 控制面板,其中将列出应用程序中的所有测试。它将向您展示一些预先编写的测试。查看它们以查看您是否喜欢,然后稍后删除它们。
如果你注意到,当你之前运行命令时,你的根项目文件夹中也创建了一个名为 `cypress` 的新文件夹。
|cypress
-|fixtures
-|integrations
-|plugins
-|support
我们将专注于 `integrations` 文件夹,因为那将是您编写测试的文件夹。现在,在我们实际编写测试之前,让我们安装另一个库,它将帮助我们从实际用户的角度编写测试
npm install @testing-library/cypress --save-dev
为了让 Cypress 知道我们新安装的 `testing-library`,我们需要将单个命令添加到 `cypress/support/commands.js` 中
import "@testing-library/cypress/add-commands";
现在,我们准备开始编写第一个端到端测试!
为了减少我们自己创建功能应用程序的麻烦,让我们使用生产就绪的 Mattermost 应用程序。要设置您的 Mattermost 开发环境,请查看他们出色的开发文档。文档相当直观,但如果您确实遇到问题,可以随时前往社区聊天寻求帮助。
Mattermost 广泛使用 Cypress 自动化测试。阅读更多关于Mattermost 如何使用 Cypress 的信息。
编写您的第一个测试
现在您已准备好编写和运行一些测试,让我们在您最喜欢的代码编辑器中打开 `mattermost-webapp`。这是一个庞大的代码库,因为它是一个庞大而复杂的产品,但请花一些时间,一切都会开始变得有意义。我们将为 Mattermost 的登录页面编写一些测试。
接下来,我们进入 `e2e/cypress` 文件夹,然后进入 `integrations` 文件夹。您可以在 `integrations` 内部看到多个测试文件夹。每个文件夹都代表应用程序正在测试的某个功能或组件。找到 `signin_authentication` 文件夹并查找 `login_spec.js` 文件。
describe('Login page', () => {
let config;
let testUser;
before(() => {
// Disable other auth options
const newSettings = {
Office365Settings: { Enable: false },
LdapSettings: { Enable: false },
};
cy.apiUpdateConfig(newSettings).then((data) => {
({ config } = data);
});
// # Create new team and users
cy.apiInitSetup().then(({ user }) => {
testUser = user;
cy.apiLogout();
cy.visit('/login');
});
});
it('should render', () => {
// * Check that the login section is loaded
cy.get('#login_section').should('be.visible');
// * Check the title
cy.title().should('include', config.TeamSettings.SiteName);
});
it('should match elements, body', () => {
// * Check elements in the body
cy.get('#login_section').should('be.visible');
cy.get('#site_name').should('contain', config.TeamSettings.SiteName);
cy.get('#site_description').should('contain', config.TeamSettings.CustomDescriptionText);
cy.get('#loginId')
.should('be.visible')
.and(($loginTextbox) => {
const placeholder = $loginTextbox[0].placeholder;
expect(placeholder).to.match(/Email/);
expect(placeholder).to.match(/Username/);
});
cy.get('#loginPassword')
.should('be.visible')
.and('have.attr', 'placeholder', 'Password');
cy.get('#loginButton').should('be.visible').and('contain', 'Sign in');
cy.get('#login_forgot').should('contain', 'I forgot my password');
});
it('should match elements, footer', () => {
// * Check elements in the footer
cy.get('#footer_section').should('be.visible');
cy.get('#company_name').should('contain', 'Mattermost');
cy.get('#copyright')
.should('contain', '(c) 2015-')
.and('contain', 'Mattermost, Inc.');
cy.get('#about_link')
.should('contain', 'About')
.and('have.attr', 'href', config.SupportSettings.AboutLink);
cy.get('#privacy_link')
.should('contain', 'Privacy')
.and('have.attr', 'href', config.SupportSettings.PrivacyPolicyLink);
cy.get('#terms_link')
.should('contain', 'Terms')
.and('have.attr', 'href', config.SupportSettings.TermsOfServiceLink);
cy.get('#help_link')
.should('contain', 'Help')
.and('have.attr', 'href', config.SupportSettings.HelpLink);
});
it('should login then logout by test user', () => {
// # Enter username on Email or Username input box
cy.get('#loginId').should('be.visible').type(testUser.username);
// # Enter password on "Password" input box
cy.get('#loginPassword').should('be.visible').type(testUser.password);
// # Click "Sign in" button
cy.get('#loginButton').should('be.visible').click();
// * Check that the Signin button change with rotating icon and "Signing in..." text
cy.get('#loadingSpinner')
.should('be.visible')
.and('contain', 'Signing in...');
// * Check that it login successfully and it redirects into the main channel page
cy.get('#channel_view').should('be.visible');
// # Click hamburger main menu button
cy.get('#sidebarHeaderDropdownButton').click();
cy.get('#logout').should('be.visible').click();
// * Check that it logout successfully and it redirects into the login page
cy.get('#login_section').should('be.visible');
cy.location('pathname').should('contain', '/login');
});
});
如果你快速浏览一下,你会发现大量使用了元素 ID。这是 Mattermost 团队刚开始进行端到端测试时编写的第一个测试。团队现在使用的许多最佳实践可以在更近期的测试文件中找到。但不知何故,这个特定的测试没有改变,所以这是一个很好的机会,我们可以提交一个改进的测试作为拉取请求。
每个测试规范都包含在 `describe` 块中,其主标题是我们的测试套件,后面跟着许多包含单独测试的 `it` 块。您还可以看到 `before` 的使用,它旨在在测试开始时运行。此时,我们无需深入了解太多细节,只需知道它会创建一个新的测试用户并重定向到登录页面即可。清除 `describe` 块中除 `before` 块之外的所有内容。添加一个带有标题“`should render all elements of the page`”的新 `it` 块。
describe('Login page', () => {
let config;
let testUser;
before(() => {
// Disable other auth options
const newSettings = {
Office365Settings: {Enable: false},
LdapSettings: {Enable: false},
};
cy.apiUpdateConfig(newSettings).then((data) => {
({config} = data);
});
// # Create new team and users
cy.apiInitSetup().then(({user}) => {
testUser = user;
cy.apiLogout();
cy.visit('/login');
});
});
it('should render all elements of the page', () => {
// start writing here
});
});
我们第一个测试的目的是检查页面上是否渲染了所有元素。首先,我们验证 URL 是否与登录页面匹配。Cypress 提供了一个命令 — cy.url — 来获取页面的当前 URL
// * Verify URL is of login page
cy.url().should('include', '/login');
注意 `should` 的用法。它是断言——当提供合适的链接器和值时——创建 Cypress 验证的测试用例。在上面的代码中,它翻译成:URL **应该**包含 */login*。Cypress 运行此用例,如果失败,则测试失败——反之亦然。
我们还可以使用 cy.title 命令验证文档的标题。在 `before` 块中,我们可以访问创建的团队 `config` 对象,其中包含页面的文档标题。
// * Verify title of the document is correct
cy.title().should('include', config.TeamSettings.SiteName);
现在,我们需要检查页面是否包含登录所需的基本元素,例如,电子邮件/用户名字段、密码字段和提交按钮。
// * Verify email/username field is present
cy.findByPlaceholderText('Email or Username').should('exist').and('be.visible');
// * Verify password is present
cy.findByPlaceholderText('Password').should('exist').and('be.visible');
// * Verify sign in button is present
cy.findByText('Sign in').should('exist').and('be.visible');
我们通过占位符查找输入字段,并通过文本查找提交按钮,这与实际用户查找它们的方式类似。要添加多个断言,我们可以链式调用 `and` 函数。具体来说,在上述情况下,我们正在验证元素是否对用户可见,并且没有滚出屏幕。
我们现在将尝试找出页面是否有忘记密码链接,以及它是否指向正确的 URL
// * Verify forgot password link is present
cy.findByText('I forgot my password.').should('exist').and('be.visible').
parent().should('have.attr', 'href', '/reset_password');
在上面的断言中我们做了一些新的事情;注意 parent 的用法。为了回答这个问题,让我们看看“我忘记密码”的 HTML 结构
<a href="/reset_password">
<span>I forgot my password.</span>
</a>
通过文本查找将返回 `span` 元素——而不是实际包含链接的锚元素。一个好的做法是将其用在锚本身内部。但在这里,由于翻译原因,团队不得不使用上述方法。`parent` 命令有助于从 `span` 上移一级,然后我们可以检查父 `` 上是否存在正确的 URL。
到目前为止,这是我们编写的完整测试
it('should render all elements of the page', () => {
// * Verify URL is of login page
cy.url().should('include', '/login');
// * Verify title of the document is correct
cy.title().should('include', config.TeamSettings.SiteName);
// * Verify email/username field is present
cy.findByPlaceholderText('Email or Username').should('exist').and('be.visible');
// * Verify password is present
cy.findByPlaceholderText('Password').should('exist').and('be.visible');
// * Verify sign in button is present
cy.findByText('Sign in').should('exist').and('be.visible');
// * Verify forgot password link is present
cy.findByText('I forgot my password.').should('exist').and('be.visible').
parent().should('have.attr', 'href', '/reset_password');
});
要运行测试,您可以打开终端并在 `e2e/` 文件夹中执行 `open cypress` 命令。Cypress 仪表板打开后,找到测试文件名并双击运行测试
cd e2e/
npm run cypress:open
如果您的测试运行并通过,恭喜您使用 Cypress 编写了端到端测试!您可以继续为登录编写多个场景并查看您的测试运行。
编写另一个测试用例
让我们编写另一个场景,其中我们输入错误或不存在的用户凭据进行登录。
打开一个新的 `it` 块,并将其命名为“`Should show error with invalid email/username and password`”。创建随机的用户名和密码常量
it('Should show error with invalid email/username and password', () => {
const invalidEmail = `${Date.now()}-user`;
const invalidPassword = `${Date.now()}-password`;
});
我们有来自 `before` 块的实际用户和密码信息。为了确保,让我们验证我们生成的凭据与实际用户的凭据不相同。
it('Should show error with invalid email/username and password', () => {
const invalidEmail = `${Date.now()}-user`;
const invalidPassword = `${Date.now()}-password`;
+ // # Lets verify generated email is not an actual email/username
+ expect(invalidEmail).to.not.equal(testUser.username);
+ // # Lets verify generated password is not an actual password
+ expect(invalidPassword).to.not.equal(testUser.password);
});
`expect` 断言来自 Cypress 内置的 ChaiJS 库。在相应的字段中输入无效的电子邮件/用户名和密码。Cypress 允许您通过 `type` 命令输入字段值。它还为您提供了 `click` 命令,您可以在提交按钮上使用。
it('Should show error with invalid email/username and password', () => {
const invalidEmail = `${Date.now()}-user`;
const invalidPassword = `${Date.now()}-password`;
// # Lets verify generated email is not an actual email/username
expect(invalidEmail).to.not.equal(testUser.username);
// # Lets verify generated password is not an actual password
expect(invalidPassword).to.not.equal(testUser.password);
+ // # Enter invalid email/username in the email field
+ cy.findByPlaceholderText('Email or Username').clear().type(invalidEmail);
+ // # Enter invalid password in the password field
+ cy.findByPlaceholderText('Password').clear().type(invalidPassword);
+ // # Hit enter to login
+ cy.findByText('Sign in').click();
});
现在,我们只需验证是否收到了正确的错误消息。
it('Should show error with invalid email/username and password', () => {
const invalidEmail = `${Date.now()}-user`;
const invalidPassword = `${Date.now()}-password`;
// # Lets verify generated email is not an actual email/username
expect(invalidEmail).to.not.equal(testUser.username);
// # Lets verify generated password is not an actual password
expect(invalidPassword).to.not.equal(testUser.password);
// # Enter invalid email/username in the email field
cy.findByPlaceholderText('Email or Username').clear().type(invalidEmail);
// # Enter invalid password in the password field
cy.findByPlaceholderText('Password').clear().type(invalidPassword);
// # Hit enter to login
cy.findByText('Sign in').click();
+ // * Verify appropriate error message is displayed for incorrect email/username and password
+ cy.findByText('Enter a valid email or username and/or password.').should('exist').and
('be.visible');
});
瞧!您已经覆盖了另一个常见的测试用例。
如果您想查看我所做的其余改进,请查看此拉取请求。
到现在,您已经看到 Cypress 如何让端到端测试变得轻松愉快。所以继续在您的应用程序中引入一些端到端测试吧!