React端到端测试
下面,我们来系统的梳理关于 React 端到端测试:Cypress 的基本知识点:
一、Cypress 概述
1.1 什么是端到端测试?
端到端测试(E2E)模拟真实用户操作,验证整个应用从用户界面到后端系统的完整工作流程。它关注:
- 用户界面交互
- 业务流程验证
- 系统集成点
- 真实网络请求和响应
1.2 Cypress 的核心优势
特性 | 描述 | 优势 |
---|---|---|
实时重载 | 测试代码更改后自动重新运行 | 快速迭代开发 |
时间旅行 | 查看测试每一步的应用程序状态 | 高效调试 |
自动等待 | 智能等待元素出现和网络请求 | 减少人为等待 |
网络控制 | 拦截和模拟网络请求 | 测试边缘情况 |
视频录制 | 自动录制测试过程 | 问题复现和分析 |
一致性 | 在CI和本地环境运行相同测试 | 减少环境差异问题 |
1.3 Cypress 架构
二、环境搭建与配置
2.1 安装
npm install cypress --save-dev
2.2 项目结构
cypress/
├── fixtures/ # 测试数据
├── integration/ # 测试用例
├── plugins/ # 插件配置
├── screenshots/ # 失败截图
├── support/ # 自定义命令和全局配置
└── videos/ # 测试录像
2.3 配置文件 (cypress.json)
{"baseUrl": "http://localhost:3000","viewportWidth": 1280,"viewportHeight": 720,"video": true,"retries": {"runMode": 2,"openMode": 0},"env": {"apiUrl": "https://api.example.com","user": {"email": "test@example.com","password": "securePassword123"}}
}
2.4 启动 Cypress
# 交互模式
npx cypress open# CLI 模式
npx cypress run
三、编写测试
3.1 基本测试结构
// cypress/integration/login.spec.js
describe('登录功能测试', () => {beforeEach(() => {// 每个测试前执行cy.visit('/login');});it('使用有效凭证成功登录', () => {cy.get('#email').type(Cypress.env('user').email);cy.get('#password').type(Cypress.env('user').password);cy.get('form').submit();cy.url().should('include', '/dashboard');cy.get('.welcome-message').should('contain', '欢迎回来');});it('使用无效凭证登录失败', () => {cy.get('#email').type('invalid@example.com');cy.get('#password').type('wrongPassword');cy.get('form').submit();cy.get('.error-message').should('be.visible').and('contain', '邮箱或密码错误');});
});
四、Cypress 核心概念
4.1 命令链
Cypress 命令返回链式对象,可连续调用:
cy.get('.todo-list').find('li').first().should('have.class', 'completed').children('.toggle').should('be.checked');
4.2 断言
断言类型 | 示例 | 描述 |
---|---|---|
隐式断言 | cy.get('button').should('be.enabled') | 自动重试直到断言通过 |
显式断言 | expect(user).to.have.property('name', 'John') | 传统断言方式 |
BDD 断言 | cy.wrap(42).should('equal', 42) | 行为驱动开发风格 |
4.3 选择元素
方法 | 示例 | 推荐指数 |
---|---|---|
data-cy | cy.get('[data-cy="submit-btn"]') | ⭐⭐⭐⭐⭐ |
CSS 类 | cy.get('.btn-primary') | ⭐⭐⭐⭐ |
ID | cy.get('#login-form') | ⭐⭐⭐⭐ |
文本内容 | cy.contains('登录') | ⭐⭐⭐ |
属性 | cy.get('input[type="email"]') | ⭐⭐⭐ |
4.4 常用命令
// 导航
cy.visit('/products');
cy.go('back');
cy.reload();// 交互
cy.get('button').click();
cy.get('input').type('Hello{enter}');
cy.get('select').select('option2');// 表单
cy.get('form').submit();
cy.get('input').clear();
cy.get('input').check();
cy.get('input').uncheck();// 窗口
cy.viewport(1024, 768);
cy.scrollTo('bottom');
五、测试技术
5.1 网络请求控制
// 拦截并模拟响应
cy.intercept('GET', '/api/users', {statusCode: 200,body: [{ id: 1, name: 'Alice' },{ id: 2, name: 'Bob' }]
}).as('getUsers');// 触发请求
cy.visit('/users');// 等待请求完成
cy.wait('@getUsers').then((interception) => {expect(interception.response.statusCode).to.eq(200);
});// 修改真实响应
cy.intercept('POST', '/api/login', (req) => {req.reply((res) => {res.body.token = 'mock_token_123';return res;});
});
5.2 数据库操作
// 使用 cy.task 与 Node.js 交互
// cypress/plugins/index.js
module.exports = (on, config) => {on('task', {resetDatabase: () => {// 调用数据库重置脚本return exec('npm run db:reset');},createUser: (userData) => {// 数据库创建用户return db('users').insert(userData);}});
};// 在测试中使用
beforeEach(() => {cy.task('resetDatabase');
});it('创建新用户', () => {cy.task('createUser', { name: 'Test User', email: 'test@example.com' });cy.visit('/users');cy.contains('Test User').should('be.visible');
});
5.3 文件操作
// 上传文件
cy.get('input[type="file"]').attachFile('avatar.jpg');// 下载文件
cy.intercept('GET', '/downloads/report.pdf', {fixture: 'report.pdf'
}).as('downloadReport');cy.get('#download-btn').click();
cy.wait('@downloadReport');// 验证文件内容
cy.readFile('cypress/downloads/report.pdf').should('exist');
5.4 认证处理
// 自定义登录命令
// cypress/support/commands.js
Cypress.Commands.add('login', (email, password) => {cy.session([email, password], () => {cy.visit('/login');cy.get('#email').type(email);cy.get('#password').type(password);cy.get('form').submit();cy.url().should('include', '/dashboard');});
});// 在测试中使用
beforeEach(() => {cy.login(Cypress.env('user').email, Cypress.env('user').password);
});
六、测试组织与实践
6.1 测试组织结构
// 页面对象模式 (Page Object)
// cypress/support/pages/LoginPage.js
class LoginPage {visit() {cy.visit('/login');}fillEmail(email) {cy.get('#email').type(email);}fillPassword(password) {cy.get('#password').type(password);}submit() {cy.get('form').submit();}assertErrorMessage(message) {cy.get('.error-message').should('contain', message);}
}export default new LoginPage();// 在测试中使用
import LoginPage from '../support/pages/LoginPage';describe('登录测试', () => {it('显示错误消息', () => {LoginPage.visit();LoginPage.fillEmail('invalid@example.com');LoginPage.fillPassword('wrong');LoginPage.submit();LoginPage.assertErrorMessage('邮箱或密码错误');});
});
6.2 测试数据管理
// 使用 fixtures
// cypress/fixtures/users.json
{"admin": {"email": "admin@example.com","password": "adminPass123","role": "admin"},"customer": {"email": "customer@example.com","password": "customerPass123","role": "customer"}
}// 在测试中使用
beforeEach(() => {cy.fixture('users').as('users');
});it('管理员登录', function() {const admin = this.users.admin;cy.login(admin.email, admin.password);cy.get('.admin-dashboard').should('be.visible');
});
6.3 实践
-
选择器策略:
- 使用
data-cy
属性作为首选选择器 - 避免使用易变的 CSS 类名
- 优先使用文本内容而非位置选择器
- 使用
-
测试设计:
- 每个测试只验证一个功能点
- 使用描述性的测试名称
- 避免测试依赖(每个测试独立运行)
-
性能优化:
- 使用
cy.session()
缓存登录状态 - 减少不必要的页面加载
- 并行运行测试
- 使用
七、持续集成(CI)
7.1 GitHub Actions 配置
name: E2E Testson: [push, pull_request]jobs:cypress-run:runs-on: ubuntu-lateststeps:- name: Checkoutuses: actions/checkout@v2- name: Install dependenciesrun: npm ci- name: Start serverrun: npm start &- name: Run Cypress testsuses: cypress-io/github-action@v4with:start: npm startwait-on: 'http://localhost:3000'browser: chromerecord: trueenv:CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
7.2 并行运行测试
npx cypress run --record --key <record-key> --parallel
八、调试技巧
8.1 交互式调试
it('调试示例', () => {cy.visit('/');// 暂停测试cy.debug();// 或使用 .then 调试cy.get('button').then(($btn) => {debugger; // 在浏览器开发者工具中调试$btn.click();});
});
8.2 时间旅行
Cypress 测试运行器显示每个命令的快照:
- 点击命令查看应用状态
- 悬停在命令上查看 DOM 状态
- 使用 “before” 和 “after” 查看变化
8.3 控制台输出
cy.get('table').then(($table) => {console.log('Table content:', $table.text());// Cypress 日志Cypress.log({name: 'table',message: $table.text()});
});
九、常见问题解决方案
9.1 元素不可见错误
问题:CypressError: Timed out retrying: expected '<button>' to be 'visible'
解决方案:
// 确保元素在视图中
cy.get('button').scrollIntoView().should('be.visible');// 检查是否被覆盖
cy.get('button').click({ force: true });// 等待元素变为可见
cy.get('.loading-spinner', { timeout: 10000 }).should('not.exist');
cy.get('button').should('be.visible');
9.2 跨域问题
问题:CypressError: cy.visit() failed because you are attempting to visit a second unique domain
解决方案:
// 禁用 Web 安全(谨慎使用)
{"chromeWebSecurity": false
}// 或使用代理处理跨域请求
cy.intercept('https://api.example.com/*', (req) => {req.headers['origin'] = 'http://localhost:3000';
});
9.3 测试稳定性
问题:测试有时通过,有时失败
解决方案:
// 增加重试次数
{"retries": {"runMode": 2,"openMode": 0}
}// 使用自定义重试逻辑
Cypress.Commands.overwrite('should', (originalFn, subject, expectation, ...args) => {const options = {interval: 500,timeout: 10000};return originalFn(subject, expectation, ...args, options);
});
十、Cypress 生态系统
10.1 常用插件
插件 | 用途 | 安装 |
---|---|---|
cypress-real-events | 真实浏览器事件 | npm install cypress-real-events |
cypress-axe | 无障碍测试 | npm install cypress-axe |
cypress-image-snapshot | 视觉回归测试 | npm install cypress-image-snapshot |
cypress-grep | 测试过滤 | npm install cypress-grep |
cypress-fail-fast | 快速失败 | npm install cypress-fail-fast |
10.2 报告生成
# 安装 Mochawesome 报告器
npm install mochawesome mochawesome-merge mochawesome-report-generator --save-dev
// cypress.json
{"reporter": "mochawesome","reporterOptions": {"reportDir": "cypress/reports","overwrite": false,"html": false,"json": true}
}
十一、总结
Cypress 测试金字塔
总结
-
测试策略:
- 优先覆盖关键用户流程
- 避免过度测试实现细节
- 结合单元和集成测试
-
技术选择:
- 使用页面对象模式提高可维护性
- 利用网络拦截测试边缘情况
- 实现自定义命令复用逻辑
-
持续改进:
- 定期审查测试覆盖率
- 修复不稳定的测试
- 集成到CI/CD流水线
-
团队协作:
- 开发与测试共同编写测试
- 使用相同测试语言(Gherkin可选)
- 共享测试报告和视频