一、E2E、E2E测试和Cypress框架的三言两语
- E2E: 即 End-to-End,中文为 “端到端”。可以想象有一条管道,起点是用户的操作,比如在手机或电脑上点击一个按钮、输入一些信息等,这就是一端。然后信息沿着管道经过一系列的环节,包括软件的前端界面展示、与后端服务器的通信、数据库的存储和读取等各种复杂的处理过程,最终,管道的终点是用户得到一个反馈结果,比如看到一个新的页面、收到一条确认信息等,这就是另一端。
- E2E测试:从用户操作的一端到用户获得反馈结果的另一端的整个过程,涵盖了应用的前端界面、后端逻辑、数据库交互等多个方面,旨在确保这个完整的链条上,每一个环节都能正常工作,要求从一端到另一端一样畅通无阻,所以称为 “端到端” 测试。
- Cypress 是一个流行的前端自动化测试框架,是常见的 E2E 测试框架之一,具有以下主要特点:
- 运行速度快:Cypress 能够在很短的时间内执行测试用例,这是因为它在与浏览器同一进程中运行,直接与浏览器的 DOM 进行交互,避免了网络延迟和通信开销。
- 实时重新加载:在开发过程中,当你对测试代码进行修改时,Cypress 可以实时重新加载测试,无需重新启动整个测试套件,大大提高了开发效率。
- 简洁的 API:Cypress 提供了一套简洁直观的 API,使得编写测试用例变得非常容易。测试用例的代码可读性高,易于理解和维护。
- 强大的断言库:Cypress 内置了丰富的断言方法,可以轻松验证页面的状态、元素的属性和文本内容等。
- 测试运行器:Cypress 提供了一个可视化的测试运行器界面,在测试执行过程中,你可以实时观察测试的进度、页面的变化以及错误信息。这使得调试测试用例变得更加容易。
二、初体验Cypress框架
1. 为了快速体验Cypress是怎么进行自动化测试的,这里使用的是Cypress团队提供的example进行,需要用到Git客户端从Github上clone到本地代码库中。
git clone https://github.com/cypress-io/cypress-example-recipes
2. 确保你的项目环境中已经安装了 Node.js ,包管理器强烈推荐yarn和pnpm,可以解决依赖下载时网络的不稳定导致的下载失败(多次install即可),以及多次install时下载的依赖之间的关系版本的稳定。
yarn install
3. 依赖安装完毕后,就可以启动被测应用了,这里要测试的子项目文件夹是关于表单类型的登录。
cd examples\logging-in__html-web-forms
yarn start
4. 启动成功后,命令行窗口将显示服务器的地址和端口,可以打开浏览器进行访问地址查看表单的网页内容。
5. 这个时候就可以进行测试用例的编写了,Cypress团队为我们提供了该表单的测试用例的范本,在logging-in__html-web-forms目录下的cypress\e2e\logging-in-html-web-form-spec.cy.js。
describe('Logging In - HTML Web Form', function () {
// we can use these values to log in
const username = 'jane.lane'
const password = 'password123'
context('Unauthorized', function () {
it('is redirected on visit to /dashboard when no session', function () {
// we must have a valid session cookie to be logged
// in else we are redirected to /unauthorized
cy.visit('/dashboard')
cy.get('h3').should(
'contain',
'You are not logged in and cannot access this page'
)
cy.url().should('include', 'unauthorized')
})
it('is redirected using cy.request', function () {
cy.request({
url: '/dashboard',
followRedirect: false, // turn off following redirects automatically
}).then((resp) => {
// should have status code 302
expect(resp.status).to.eq(302)
expect(resp.redirectedToUrl).to.eq('http://localhost:7077/unauthorized')
})
})
})
context('HTML form submission', function () {
beforeEach(function () {
cy.visit('/login')
})
it('displays errors on login', function () {
// incorrect username on purpose
cy.get('input[name=username]').type('jane.lae')
cy.get('input[name=password]').type('password123{enter}')
// we should have visible errors now
cy.get('p.error')
.should('be.visible')
.and('contain', 'Username and/or password is incorrect')
// and still be on the same URL
cy.url().should('include', '/login')
})
it('redirects to /dashboard on success', function () {
cy.get('input[name=username]').type(username)
cy.get('input[name=password]').type(password)
cy.get('form').submit()
// we should be redirected to /dashboard
cy.url().should('include', '/dashboard')
cy.get('h1').should('contain', 'jane.lane')
// and our cookie should be set to 'cypress-session-cookie'
cy.getCookie('cypress-session-cookie').should('exist')
})
})
context('HTML form submission with cy.request', function () {
it('can bypass the UI and yet still test log in', function () {
cy.request({
method: 'POST',
url: '/login', // baseUrl will be prepended to this url
form: true, // indicates the body should be form urlencoded and sets Content-Type: application/x-www-form-urlencoded headers
body: {
username,
password,
},
})
// just to prove we have a session
cy.getCookie('cypress-session-cookie').should('exist')
})
})
context('Reusable "login" custom command', function () {
Cypress.Commands.add('loginByForm', (username, password) => {
Cypress.log({
name: 'loginByForm',
message: `${username} | ${password}`,
})
return cy.request({
method: 'POST',
url: '/login',
form: true,
body: {
username,
password,
},
})
})
beforeEach(function () {
// login before each test
cy.loginByForm(username, password)
})
it('can visit /dashboard', function () {
// after cy.request, the session cookie has been set
// and we can visit a protected page
cy.visit('/dashboard')
cy.get('h1').should('contain', 'jane.lane')
})
it('can visit /users', function () {
// or another protected page
cy.visit('/users')
cy.get('h1').should('contain', 'Users')
})
it('can simply request other authenticated pages', function () {
cy.request('/admin')
.its('body')
.should('include', '<h1>Admin</h1>')
})
})
})
- 测试用例讲解
这段测试用例是为了测试一个基于 HTML 表单提交的登录系统的功能和行为。其主要目的是确保未经授权的用户无法访问特定页面,验证登录表单的错误处理和成功登录后的重定向,以及测试通过
cy.request
进行快速登录并验证各种状态的正确性。同时,还创建了一个可复用的自定义命令来简化登录过程并测试受保护页面的访问。全面地测试了登录系统的各个方面,包括未经授权的访问、表单提交的错误处理、成功登录后的行为以及可复用的测试逻辑,有助于确保登录功能的稳定可靠。1. Unauthorized 部分
- 第一个测试用例:通过直接使用
cy.visit('/dashboard')
尝试访问受保护的仪表板页面,验证当没有有效会话时,用户被重定向到/unauthorized
页面,并检查页面上的错误提示信息。这确保了未经授权的访问被正确处理。- 第二个测试用例:使用
cy.request
以编程方式请求/dashboard
页面,并设置followRedirect: false
以检查服务器的响应状态码和重定向的目标 URL。这是另一种方式来验证未经授权的访问会导致重定向到正确的页面。2. HTML form submission 部分
- 第一个测试用例:在登录页面故意输入错误的用户名,然后提交表单。这个测试验证了错误处理机制,确保在用户名或密码不正确时显示错误消息并且用户仍然停留在登录页面。
- 第二个测试用例:输入正确的用户名和密码并提交表单。这个测试检查了成功登录后的行为,包括重定向到
/dashboard
页面、显示正确的用户信息以及设置了正确的会话 cookie。3. HTML form submission with cy.request 部分
- 这个测试用例展示了如何使用
cy.request
绕过用户界面直接进行登录操作。通过发送一个 POST 请求到/login
路径,模拟表单提交。这样可以更快地测试登录功能,因为不需要加载整个页面和相关资源,节省了测试时间。4. Reusable "login" custom command 部分
- 创建了一个名为
loginByForm
的自定义命令,它封装了登录的逻辑,接受用户名和密码作为参数,并使用cy.request
进行登录。这个自定义命令可以在多个测试用例中复用,简化了登录过程。- 在这部分的三个测试用例中,分别展示了在登录后访问受保护的页面(
/dashboard
和/users
)以及通过cy.request
直接请求受保护的页面(/admin
)并检查响应体的方法。这确保了成功登录后可以访问受保护的资源。
- 用到的cypress语法糖
1. describe 和 context 用于组织一组相关的测试。
// describe接受一个描述字符串和一个包含测试的函数。 describe('Logging In - HTML Web Form', function () {...}); // context类似于 describe,用于进一步分组相关的测试,通常用于描述特定的测试场景或条件。 context('Unauthorized', function () {...});
2. it 定义一个具体的测试用例,接受一个描述字符串和一个包含测试逻辑的函数。
it('is redirected on visit to /dashboard when no session', function () {...});
3. 变量声明和赋值
// 声明并初始化常量变量,用于存储用户名和密码。 const username = 'jane.lane'; const password = 'password123';
4. cy.visit 访问指定的 URL。
// 在这个测试用例中,用于尝试访问受保护的仪表板页面。 cy.visit('/dashboard');
5. cy.get 根据 CSS 选择器查找元素,可以对找到的元素进行各种断言和操作。
// 找到特定的输入框元素,并在其中输入文本。 cy.get('input[name=username]').type('jane.lae');
6. cy.should:对找到的元素进行断言。
// 检查 h3 元素是否包含特定的文本。 cy.get('h3').should('contain', 'You are not logged in and cannot access this page');
7. cy.url:获取当前页面的 URL,并进行断言。
// 检查 URL 是否包含特定的字符串。 cy.url().should('include', 'unauthorized')
8. cy.request:发送一个 HTTP 请求。可以配置请求的参数,如 URL、方法、是否跟随重定向等。在 .then 回调中可以处理响应并进行断言。
cy.request({ url: '/dashboard', followRedirect: false }).then((resp) => {...});
9. 自定义命令 Cypress.Commands.add
// 创建一个自定义命令。 // 可以在测试中使用这个命令来简化重复的操作,这里是封装了登录的逻辑。 Cypress.Commands.add('loginByForm', (username, password) => {...});
10. cy.log:记录一个自定义的日志消息,有助于在测试运行时了解测试的执行过程。
Cypress.log({ name: 'loginByForm', message: ${username} | ${password} })
11. cy.loginByForm:调用自定义的登录命令
// 传入用户名和密码参数,执行登录操作。 cy.loginByForm(username, password);
12. cy.visit、cy.get 和更多断言
在后续的测试用例中,继续使用 cy.visit 访问页面,cy.get 查找元素,并结合各种断言来验证页面的内容和状态。例如,检查页面标题、元素的可见性、包含特定文本等。
6. 在命令行中运行以下命令来运行 Cypress 测试
yarn run cypress:open
7. Cypress 运行成功并打开的Cypress可视化界面。
8. 点击Start E2E Testing in Chrome,选择logging-in-html-web-form-spec.cy.js测试脚本运行,在Chrome浏览器中查看测试结果。
三、延伸:Vue 项目中集成端到端(E2E)测试
1. 选择测试框架
常见的用于 Vue 项目的 E2E 测试框架有 Cypress 和 Puppeteer。Cypress 提供了强大的功能和良好的用户体验,而 Puppeteer 则是由 Google 开发的用于控制无头浏览器的工具,可以用于编写 E2E 测试。
2. 安装测试框架
- 如果选择 Cypress:
- 确保你的项目环境中已经安装了 Node.js 和 npm。
- 在项目目录下运行以下命令安装 Cypress:
npm install cypress --save-dev
- 如果选择 Puppeteer:
- 运行以下命令安装 Puppeteer:
npm install puppeteer --save-dev
3. 配置测试框架
- Cypress:
- 运行
npx cypress open
首次启动 Cypress,它会进行一些初始化设置并在项目根目录下创建一个cypress
文件夹。 - 在
cypress/support/index.js
文件中,可以导入必要的模块和设置全局变量等。 - 在
cypress/integration
文件夹中创建测试文件,文件名通常以.spec.js
结尾。
- 运行
-
Puppeteer:
- 创建一个测试脚本文件,例如
test.js
。在脚本中,可以使用 Puppeteer 的 API 来启动浏览器、导航到应用页面、执行操作并验证结果。
- 创建一个测试脚本文件,例如
4. 编写测试用例
- Cypress:
- 使用 Cypress 的命令和断言来模拟用户行为并验证应用的行为。例如:
describe('My Vue App', () => {
it('should display the home page', () => {
cy.visit('/');
cy.contains('Welcome to my app');
});
});
- Puppeteer:
- 在测试脚本中,使用 Puppeteer 的 API 进行操作。例如:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://localhost:8080');
const title = await page.title();
expect(title).to.equal('Your Vue App Title');
await browser.close();
})();
5. 运行测试
-
Cypress:
- 在命令行中运行
npx cypress run
可以在无头模式下运行测试。或者运行npx cypress open
打开 Cypress 测试运行器界面,手动选择测试用例进行运行。
- 在命令行中运行
-
Puppeteer:
- 在命令行中运行
node test.js
来执行测试脚本。
- 在命令行中运行
6. 与持续集成和持续部署(CI/CD)工具集成
将 E2E 测试集成到持续集成和持续部署(CI/CD)流程中,以便在每次代码提交或构建时自动运行测试,根据使用的 CI 工具(如 Jenkins、Travis CI 等),配置相应的任务来运行 E2E 测试,确保 CI 环境中安装了所需的测试框架和依赖项。
通过以上步骤,你可以在 Vue 项目中实现端到端测试,确保应用在真实的用户场景下正常工作,在编写测试用例时,要尽可能覆盖各种用户场景和边缘情况,以提高测试的有效性和可靠性。