Cypress 最佳实践 #
测试组织 #
目录结构 #
text
cypress/
├── e2e/
│ ├── auth/ # 按功能模块组织
│ │ ├── login.cy.js
│ │ ├── register.cy.js
│ │ └── password-reset.cy.js
│ ├── user/
│ │ ├── profile.cy.js
│ │ └── settings.cy.js
│ └── api/
│ └── users.cy.js
├── support/
│ ├── commands/ # 自定义命令
│ ├── pages/ # 页面对象
│ ├── components/ # 组件对象
│ └── utils/ # 工具函数
├── fixtures/ # 测试数据
└── plugins/ # 插件
测试文件命名 #
javascript
// ✅ 好的命名 - 清晰表达测试内容
login.cy.js
user-profile.cy.js
api-users.cy.js
// ❌ 不好的命名
test1.cy.js
temp.cy.js
new.cy.js
测试套件组织 #
javascript
// ✅ 好的组织 - 清晰的层次结构
describe('用户管理', () => {
describe('用户列表', () => {
context('当用户已登录', () => {
it('应该显示用户列表', () => {});
});
context('当用户未登录', () => {
it('应该跳转到登录页面', () => {});
});
});
describe('用户详情', () => {
it('应该显示用户信息', () => {});
});
});
选择器策略 #
使用 data 属性 #
html
<!-- ✅ 好的做法 -->
<button data-cy="submit-button">提交</button>
<input data-cy="email-input" type="email" />
<div data-cy="error-message">错误</div>
javascript
// ✅ 好的做法 - 使用 data 属性
cy.get('[data-cy="submit-button"]').click();
cy.get('[data-cy="email-input"]').type('test@example.com');
// ❌ 不好的做法 - 依赖样式类
cy.get('.btn.btn-primary.btn-lg').click();
cy.get('.form-control.email').type('test@example.com');
// ❌ 不好的做法 - 依赖 DOM 结构
cy.get('body > div > form > div > button').click();
选择器优先级 #
text
1. [data-cy="..."] ⭐⭐⭐⭐⭐ 最推荐
2. [data-test="..."] ⭐⭐⭐⭐
3. [data-testid="..."] ⭐⭐⭐⭐
4. #id ⭐⭐⭐
5. .class ⭐⭐
6. tag.class ⭐
7. DOM 结构选择器 ❌ 避免
测试数据管理 #
使用 Fixtures #
javascript
// ✅ 好的做法 - 使用 fixture 管理数据
cy.fixture('users').then((users) => {
cy.intercept('GET', '/api/users', users);
});
// ❌ 不好的做法 - 硬编码数据
cy.intercept('GET', '/api/users', [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' }
]);
数据隔离 #
javascript
// ✅ 好的做法 - 每个测试独立
beforeEach(() => {
cy.resetDatabase();
cy.login('testuser', 'password');
});
afterEach(() => {
cy.cleanupTestData();
});
// ❌ 不好的做法 - 测试之间共享状态
let sharedData;
it('测试1', () => {
sharedData = 'data';
});
it('测试2', () => {
// 依赖测试1的结果
cy.log(sharedData);
});
异步处理 #
避免固定等待 #
javascript
// ✅ 好的做法 - 等待条件
cy.get('.loading').should('not.exist');
cy.get('.data').should('be.visible');
// ❌ 不好的做法 - 固定等待
cy.wait(2000);
cy.get('.data').click();
正确使用 then() #
javascript
// ✅ 好的做法 - 在 then() 中处理值
cy.get('.user-id').then(($el) => {
const userId = $el.text();
cy.request(`/api/users/${userId}`);
});
// ❌ 不好的做法 - 直接使用返回值
const userId = cy.get('.user-id').text(); // 错误!
网络请求 #
使用 intercept #
javascript
// ✅ 好的做法 - 使用 intercept 模拟
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers');
cy.visit('/users');
cy.wait('@getUsers');
// ❌ 不好的做法 - 依赖真实 API
cy.visit('/users'); // 可能因网络问题失败
验证请求 #
javascript
// ✅ 好的做法 - 验证请求和响应
cy.intercept('POST', '/api/users').as('createUser');
cy.get('.submit').click();
cy.wait('@createUser').then((interception) => {
expect(interception.request.body.name).to.eq('John');
expect(interception.response.statusCode).to.eq(201);
});
测试断言 #
清晰的断言 #
javascript
// ✅ 好的做法 - 清晰的断言
cy.get('.status').should('contain', '成功');
cy.get('.user-name').should('have.text', 'John Doe');
cy.get('.error').should('be.visible');
// ❌ 不好的做法 - 模糊的断言
cy.get('.status').should('exist');
cy.get('.user-name').should('not.be.empty');
合理的断言数量 #
javascript
// ✅ 好的做法 - 验证关键行为
it('登录成功后跳转到首页', () => {
cy.login('user', 'pass');
cy.url().should('include', '/dashboard');
cy.get('.welcome').should('be.visible');
});
// ❌ 不好的做法 - 过度断言
it('验证所有细节', () => {
cy.get('.header').should('have.css', 'background-color', 'rgb(0, 0, 0)');
cy.get('.header').should('have.css', 'height', '60px');
cy.get('.header').should('have.css', 'padding', '10px');
// ... 太多断言
});
自定义命令 #
封装常用操作 #
javascript
// ✅ 好的做法 - 封装为自定义命令
Cypress.Commands.add('login', (username, password) => {
cy.session([username, password], () => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: { username, password }
}).then((response) => {
window.localStorage.setItem('token', response.body.token);
});
});
});
// 使用
cy.login('testuser', 'password');
命令命名 #
javascript
// ✅ 好的命名
Cypress.Commands.add('loginAsAdmin', () => {});
Cypress.Commands.add('waitForPageLoad', () => {});
Cypress.Commands.add('createTestUser', () => {});
// ❌ 不好的命名
Cypress.Commands.add('doIt', () => {});
Cypress.Commands.add('test', () => {});
页面对象模式 #
使用页面对象 #
javascript
// ✅ 好的做法 - 使用页面对象
const loginPage = new LoginPage();
loginPage
.visit()
.login('user', 'pass');
// ❌ 不好的做法 - 直接操作
cy.visit('/login');
cy.get('#username').type('user');
cy.get('#password').type('pass');
cy.get('button').click();
性能优化 #
会话缓存 #
javascript
// ✅ 好的做法 - 使用 session 缓存登录
Cypress.Commands.add('login', (username, password) => {
cy.session([username, password], () => {
// 登录逻辑
}, {
cacheAcrossSpecs: true
});
});
并行执行 #
javascript
// cypress.config.js
module.exports = {
e2e: {
specPattern: 'cypress/e2e/**/*.cy.js',
// 配置并行执行
}
};
// 运行时指定
// npx cypress run --parallel
减少视频录制 #
javascript
// cypress.config.js
module.exports = {
e2e: {
video: false, // 禁用视频
screenshotOnRunFailure: true // 仅失败时截图
}
};
CI/CD 集成 #
GitHub Actions 配置 #
yaml
name: Cypress Tests
on: [push, pull_request]
jobs:
cypress:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run Cypress tests
uses: cypress-io/github-action@v6
with:
browser: chrome
start: npm start
wait-on: 'http://localhost:3000'
- name: Upload artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: cypress-results
path: |
cypress/screenshots
cypress/videos
缓存优化 #
yaml
- name: Cache Cypress binary
uses: actions/cache@v3
with:
path: ~/.cache/Cypress
key: cypress-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
错误处理 #
处理应用错误 #
javascript
// 忽略特定错误
Cypress.on('uncaught:exception', (err) => {
if (err.message.includes('ResizeObserver')) {
return false; // 忽略 ResizeObserver 错误
}
return true;
});
重试机制 #
javascript
// 配置重试
// cypress.config.js
module.exports = {
e2e: {
retries: {
runMode: 2, // CI 模式重试 2 次
openMode: 0 // 开发模式不重试
}
}
};
测试数据清理 #
测试前后清理 #
javascript
describe('用户测试', () => {
before(() => {
cy.resetDatabase();
});
beforeEach(() => {
cy.login('testuser', 'password');
});
afterEach(() => {
cy.clearCookies();
cy.clearLocalStorage();
});
after(() => {
cy.cleanupTestData();
});
});
可访问性测试 #
使用 cypress-axe #
javascript
// 安装
// npm install --save-dev cypress-axe
// cypress/support/e2e.js
import 'cypress-axe';
// 使用
it('应该没有可访问性问题', () => {
cy.visit('/');
cy.injectAxe();
cy.checkA11y();
});
调试技巧 #
使用日志 #
javascript
// ✅ 好的做法 - 添加有意义的日志
cy.log('开始登录流程');
cy.get('#username').type('user');
cy.log('用户名输入完成');
cy.get('#password').type('pass');
cy.log('密码输入完成');
使用截图 #
javascript
// 关键步骤截图
cy.get('.form').screenshot('before-submit');
cy.get('.submit').click();
cy.screenshot('after-submit');
完整示例 #
最佳实践测试文件 #
javascript
// cypress/e2e/auth/login.cy.js
import LoginPage from '../../support/pages/LoginPage';
import DashboardPage from '../../support/pages/DashboardPage';
describe('登录功能', () => {
const loginPage = new LoginPage();
const dashboardPage = new DashboardPage();
const testUser = {
username: 'testuser',
password: 'Password123!'
};
before(() => {
cy.resetDatabase();
});
beforeEach(() => {
cy.clearCookies();
cy.clearLocalStorage();
loginPage.visit();
});
after(() => {
cy.cleanupTestData();
});
context('表单验证', () => {
it('空表单应该显示验证错误', () => {
loginPage.clickLoginButton();
loginPage.verifyErrorMessage('请填写用户名');
});
it('无效邮箱格式应该显示错误', () => {
loginPage
.typeUsername('invalid-email')
.typePassword('password')
.clickLoginButton();
loginPage.verifyErrorMessage('邮箱格式不正确');
});
});
context('登录流程', () => {
it('正确的凭据应该登录成功', () => {
cy.intercept('POST', '/api/auth/login').as('login');
loginPage.login(testUser.username, testUser.password);
cy.wait('@login').then((interception) => {
expect(interception.response.statusCode).to.eq(200);
});
dashboardPage.verifyWelcomeMessage(testUser.username);
});
it('错误的密码应该显示错误提示', () => {
loginPage
.typeUsername(testUser.username)
.typePassword('wrongpassword')
.clickLoginButton();
loginPage.verifyErrorMessage('用户名或密码错误');
});
});
context('性能测试', () => {
it('登录响应时间应该小于 2 秒', () => {
cy.intercept('POST', '/api/auth/login').as('login');
loginPage.login(testUser.username, testUser.password);
cy.wait('@login').its('duration').should('be.lessThan', 2000);
});
});
});
检查清单 #
测试编写检查清单 #
- [ ] 使用 data-cy 属性选择器
- [ ] 测试之间相互独立
- [ ] 使用有意义的测试名称
- [ ] 避免固定等待
- [ ] 验证关键行为
- [ ] 清理测试数据
- [ ] 使用页面对象模式
- [ ] 添加适当的日志
- [ ] 处理异步操作正确
- [ ] 配置合理的超时
CI/CD 检查清单 #
- [ ] 配置缓存
- [ ] 设置合理的重试次数
- [ ] 上传失败截图和视频
- [ ] 配置并行执行
- [ ] 设置测试超时
- [ ] 配置环境变量
- [ ] 使用 session 缓存
总结 #
遵循这些最佳实践可以帮助你编写高质量、可维护的 Cypress 测试:
- 组织清晰:合理的目录结构和命名
- 选择器稳定:使用 data 属性
- 数据隔离:每个测试独立
- 异步正确:避免固定等待
- 断言明确:验证关键行为
- 代码复用:使用自定义命令和页面对象
- 性能优化:使用会话缓存和并行执行
- CI/CD 友好:配置合理的重试和超时
持续学习和实践这些最佳实践,你的测试代码质量会不断提升!
最后更新:2026-03-28