Skip to main content
Cypress应用

测试你的应用

info
你将学习到
  • 如何启动服务器并使用Cypress测试你的应用
  • 如何配置Cypress
  • 测试应用的策略,包括数据播种、服务器桩模块和登录

第一步:启动你的服务器

假设你已经成功 安装Cypress并在项目中 打开Cypress, 首先需要启动托管应用的本地开发服务器。

服务器地址通常类似于 **http://localhost:8080**。

caution
反模式

不要尝试从Cypress脚本中启动web服务器。阅读 最佳实践了解更多。

为什么要启动本地开发服务器?

你可能会疑惑——为什么不能直接测试已经部署到生产环境的应用程序?

虽然你确实可以测试已部署的应用,但这并不是Cypress的最佳使用场景

Cypress是为日常本地开发而构建和优化的工具。事实上,在使用Cypress一段时间后,你可能会发现它甚至能用于整个开发过程

最终,你将不仅能同时进行开发和测试,还能在获得"免费"测试的同时更快地构建应用

更重要的是——由于Cypress支持模拟网络请求,你甚至可以在不需要服务器提供有效JSON响应的情况下构建web应用。

最后但同样重要的是——试图为已构建的应用添加测试比边写测试边开发要困难得多。你可能会遇到一系列前期挑战/障碍,而这些在从一开始就编写测试时可以避免。

最重要的一点是,测试本地服务器能让你控制它们。当应用在生产环境运行时,你无法控制它。

但在开发环境中,你可以:

  • 使用快捷方式
  • 通过运行脚本播种数据
  • 暴露测试环境特定的路由
  • 禁用使自动化困难的安全功能
  • 重置服务器/数据库状态

尽管如此——你仍然可以选择两者兼顾

许多用户主要针对本地开发服务器运行大部分集成测试,同时保留一小部分冒烟测试仅针对部署的生产应用运行。

第二步:访问你的服务器

服务器启动后,就可以访问它了。

让我们删除之前教程中创建的spec.cy.js文件,因为它不再需要。

rm cypress/e2e/spec.cy.js

现在创建一个名为home_page.cy.js的测试文件。

touch cypress/e2e/home_page.cy.js

文件创建后,你应该能在测试文件列表中看到它。

包含home_page.cy.js的文件列表

现在需要在测试文件中添加以下代码来访问你的服务器:

describe('首页', () => {
it('成功加载', () => {
cy.visit('http://localhost:8080') // 将URL改为你的开发URL
})
})

点击home_page.cy.js文件,观察Cypress如何打开浏览器。

如果忘记启动服务器,你会看到如下错误:

Cypress显示cy.visit失败的错误

如果服务器已启动,你应该能看到应用加载并正常运行。

第三步:配置Cypress

提前思考一下,你会意识到需要频繁输入这个URL,因为每个测试都需要访问应用的某个页面。幸运的是,Cypress提供了 配置选项。现在就来利用它。

打开你的配置文件。初始为空,但我们可以添加baseUrl选项。

const { defineConfig } = require('cypress')

module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:8080',
},
})

这将自动为cy.visit()cy.request()命令添加此前缀。

info

每当修改配置文件,Cypress会自动重启并关闭所有打开的浏览器。这是正常现象。再次点击测试文件重新启动浏览器。

现在可以访问相对路径,省略主机名和端口。

describe('首页', () => {
it('成功加载', () => {
cy.visit('/')
})
})

很好!一切应该仍然是绿色的。

info
配置选项

Cypress还有许多其他配置选项可用于自定义其行为。比如测试文件位置、默认超时时间、环境变量、使用的报告器等。

查看配置了解更多!

测试策略

你即将开始为应用编写测试,只有你了解自己的应用,所以我们无法给出太多具体建议。

测试什么、边缘案例和接缝在哪里、可能遇到哪些回归等问题完全取决于你、你的应用和你的团队。

也就是说,现代web测试有一些常见问题,以下是你可能遇到的快速提示。

数据播种

根据应用的构建方式——你的web应用很可能会受到服务器的影响和控制。

通常现在服务器通过JSON与前端应用通信,但你也可能在运行传统的服务器端渲染HTML web应用。

通常服务器负责发送反映其状态的响应——通常存储在数据库中。

传统上在使用Selenium编写e2e测试时,在自动化浏览器之前会对服务器进行设置和拆卸

可能需要生成用户,并为其添加关联和记录。你可能熟悉使用fixtures或factories。

要测试各种页面状态——如空视图或分页视图——需要为服务器播种数据以便测试这些状态。

虽然这种策略还有很多内容,但你通常有三种方法在Cypress中实现:

如果在服务器上运行node.js,可以添加beforebeforeEach钩子执行npm任务。

describe('首页', () => {
beforeEach(() => {
// 每次测试前重置并播种数据库
cy.exec('npm run db:reset && npm run db:seed')
})

it('成功加载', () => {
cy.visit('/')
})
})

除了执行系统命令,你可能需要更多灵活性,可以仅在测试环境中暴露一系列路由。

例如,可以组合多个请求来告诉服务器你想要创建的确切状态。

describe('首页', () => {
beforeEach(() => {
// 每次测试前重置并播种数据库
cy.exec('npm run db:reset && npm run db:seed')

// 在数据库中播种一个我们可以控制的帖子
cy.request('POST', '/test/seed/post', {
title: '第一篇帖子',
authorId: 1,
body: '...',
})

// 在数据库中播种一个我们可以控制的用户
cy.request('POST', '/test/seed/user', { name: 'Jane' })
.its('body')
.as('currentUser')
})

it('成功加载', () => {
// this.currentUser现在指向cy.request()的响应体
// 可以用来登录或以某种方式使用

cy.visit('/')
})
})

虽然这种方法没有真正的问题,但它确实增加了复杂性。你需要在服务器和浏览器之间同步状态——并且总是需要在测试前设置/拆卸这些状态(这很慢)。

好消息是我们不是Selenium,也不是传统的e2e测试工具。这意味着我们不受相同限制的约束。

使用Cypress,有几种其他方法可以提供更好的体验。

服务器桩模块

另一种有效的方法是绕过服务器。

虽然你仍然会从服务器接收所有常规的HTML/JS/CSS资源,并继续以相同方式cy.visit()它——但你可以模拟来自服务器的JSON响应。

这意味着不需要重置数据库或播种所需状态,你可以强制服务器返回任何你想要的数据。这样,我们不仅避免了在服务器和浏览器之间同步状态的需要,还防止了测试改变状态。这意味着测试不会积累可能影响其他测试的状态。

另一个好处是这使你能够构建应用而无需服务器的_契约_存在。你可以按照你希望的数据结构构建它,甚至测试所有边缘案例,而无需服务器。

然而——这里可能需要两种策略的平衡(你可能应该都做)。

虽然模拟很好,但它意味着这些响应负载实际上与服务器发送的内容不匹配。不过,仍有几种有效的方法可以解决这个问题:

提前生成模拟数据

可以让服务器提前为你生成所有模拟数据。这意味着它们的数据将反映服务器实际发送的内容。

编写一个不使用模拟的e2e测试,然后模拟其余部分

另一种更平衡的方法是整合两种策略。你可能想要有一个单一测试采用真正的e2e方法且不使用模拟。它将真实使用功能——包括播种数据库和设置状态。

一旦确认它正常工作,就可以使用模拟来测试所有边缘案例和其他场景。在绝大多数情况下使用真实数据没有好处。我们建议绝大多数测试使用模拟数据。它们将快几个数量级,且复杂度低得多。

info
指南:网络请求

请阅读我们的网络请求指南以获取更全面的分析和实现方法。

登录

测试中首先要克服的(也可以说是最困难的)障碍之一就是登录应用。

没有什么比必须登录更能拖慢测试套件了,但应用中所有好的部分很可能都需要认证用户!以下是一些提示。

完整测试登录流程——但只测一次!

将注册和登录流程纳入测试覆盖是个好主意,因为它对所有用户都非常重要,你绝不希望它出问题。

登录是关键任务功能之一,应该涉及你的服务器。我们建议你像真实用户一样通过UI测试注册和登录:

以下是一个结合数据库播种的例子:

describe('登录页面', () => {
beforeEach(() => {
// 每次测试前重置并播种数据库
cy.exec('npm run db:reset && npm run db:seed')

// 在数据库中播种一个我们可以控制的用户
// 假设它为我们生成随机密码
cy.request('POST', '/test/seed/user', { username: 'jane.lane' })
.its('body')
.as('currentUser')
})

it('通过表单提交登录时设置认证cookie', function () {
// 解构this.currentUser对象
const { username, password } = this.currentUser

cy.visit('/login')

cy.get('input[name=username]').type(username)

// {enter}导致表单提交
cy.get('input[name=password]').type(`${password}{enter}`)

// 应该被重定向到/dashboard
cy.url().should('include', '/dashboard')

// 认证cookie应该存在
cy.getCookie('your-session-cookie').should('exist')

// UI应反映该用户已登录
cy.get('h1').should('contain', 'jane.lane')
})
})

你可能还想测试登录UI的以下情况:

  • 无效用户名/密码
  • 用户名已存在
  • 密码复杂度要求
  • 账户锁定/删除等边缘案例

每种情况都可能需要完整的e2e测试。

重用登录代码

此时,你可以将上述登录代码复制粘贴到每个需要认证用户的测试中。或者甚至可以将所有测试放在一个大测试文件中,并将登录代码放在beforeEach块中。但这些方法都不特别可维护,也不优雅。更好的解决方案是编写自定义的cy.login()命令

自定义命令允许你轻松封装和重用Cypress测试逻辑。它们让你可以为测试套件添加自己的功能,然后使用与内置Cypress命令相同的 可链式和异步API。 让我们将上面的登录示例改为自定义命令,并添加到cypress/support/commands.js中,以便在任何测试文件中使用:

// 在cypress/support/commands.js中

Cypress.Commands.add('login', (username, password) => {
cy.visit('/login')

cy.get('input[name=username]').type(username)

// {enter}导致表单提交
cy.get('input[name=password]').type(`${password}{enter}`, { log: false })

// 应该被重定向到/dashboard
cy.url().should('include', '/dashboard')

// 认证cookie应该存在
cy.getCookie('your-session-cookie').should('exist')

// UI应反映该用户已登录
cy.get('h1').should('contain', username)
})

// 在你的测试文件中

it('在安全页面上执行某些操作', function () {
const { username, password } = this.currentUser
cy.login(username, password)

// ...测试的其余部分
})

提高性能

你可能想知道我们关于"只登录一次"的建议怎么样了。上面的自定义命令对测试安全页面很有效,但如果有多个测试,每个测试前都登录会增加整个套件的运行时间。

幸运的是,Cypress提供了cy.session()命令,这是一个强大的性能工具,让你可以缓存与用户关联的浏览器上下文,并在多个测试中重用,无需多次登录流程!让我们修改之前示例中的自定义cy.login()命令以使用cy.session()

Cypress.Commands.add('login', (username, password) => {
cy.session(
username,
() => {
cy.visit('/login')
cy.get('input[name=username]').type(username)
cy.get('input[name=password]').type(`${password}{enter}`, { log: false })
cy.url().should('include', '/dashboard')
cy.get('h1').should('contain', username)
},
{
validate: () => {
cy.getCookie('your-session-cookie').should('exist')
},
}
)
})
info
第三方登录

如果你的应用通过Auth0Okta等第三方认证提供商实现登录,可以使用cy.origin()命令将他们的登录页面包含在认证测试中。

这里有很多内容超出了本介绍的范围。请查看cy.session()文档获取更深入的解释。

info
认证配方

登录可能比我们刚刚介绍的更复杂。

我们创建了几个配方,涵盖其他场景,如处理CSRF令牌或测试基于XHR的登录表单。

可以自由探索这些额外的登录配方。

开始吧

好了,讨论到此为止。现在就开始测试你的应用吧!

从这里你可能想探索更多指南: