Appearance
Cypress
简介
Cypress 是一个现代化的前端测试工具,专为现代 Web 应用设计,提供了端到端测试、集成测试和单元测试能力。它直接在浏览器中运行,提供了实时重载、自动等待、真实的调试体验和直观的 UI,使测试 Web 应用变得简单而可靠。
核心特性
浏览器内执行
Cypress 直接在浏览器中运行测试,而不是通过网络命令远程控制浏览器,这带来了更好的可靠性和调试体验:
bash
# 安装 Cypress
npm install cypress --save-dev
# 打开 Cypress 测试运行器
npx cypress open
# 在命令行中运行测试
npx cypress run
自动等待
Cypress 自动等待元素变为可交互状态,无需显式设置等待或超时:
javascript
// Cypress 会自动等待按钮出现并变为可点击状态
cy.get('button').click();
// 无需手动添加等待或延迟
cy.visit('/dashboard');
cy.get('.user-name').should('contain', 'John');
实时重载
Cypress 监视文件变化并自动重新运行测试,提供即时反馈:
bash
# 启动 Cypress 测试运行器,它会监视文件变化
npx cypress open
强大的调试能力
Cypress 提供了时间旅行调试、完整的错误信息和实时应用状态查看:
javascript
// 在测试中使用调试命令
cy.get('.complex-element').then(($el) => {
// 在控制台中查看元素
console.log($el);
debugger; // 触发调试器暂停
});
基本用法
编写第一个测试
javascript
// cypress/e2e/basic.cy.js
describe('首页测试', () => {
it('访问首页并验证标题', () => {
cy.visit('https://example.com');
cy.title().should('include', 'Example Domain');
cy.get('h1').should('be.visible').and('contain', 'Example Domain');
});
});
常用命令
javascript
// 导航
cy.visit('/about');
cy.go('back');
cy.reload();
// 查找元素
cy.get('.button');
cy.contains('Submit');
cy.find('.item');
// 交互
cy.get('input').type('Hello World');
cy.get('button').click();
cy.get('select').select('Option 1');
cy.get('[type="checkbox"]').check();
// 断言
cy.get('.message').should('be.visible');
cy.get('.count').should('have.text', '5');
cy.url().should('include', '/dashboard');
cy.get('.items').should('have.length', 3);
钩子函数
javascript
describe('用户功能测试', () => {
before(() => {
// 在所有测试之前运行一次
cy.log('测试套件开始');
});
beforeEach(() => {
// 每个测试之前运行
cy.visit('/');
cy.login('user@example.com', 'password123');
});
afterEach(() => {
// 每个测试之后运行
cy.clearCookies();
});
after(() => {
// 在所有测试之后运行一次
cy.log('测试套件结束');
});
it('用户可以查看仪表板', () => {
cy.get('.dashboard').should('be.visible');
});
it('用户可以编辑个人资料', () => {
cy.get('.profile-link').click();
cy.get('.edit-button').click();
cy.get('input[name="name"]').clear().type('New Name');
cy.get('.save-button').click();
cy.get('.success-message').should('be.visible');
});
});
高级功能
自定义命令
javascript
// cypress/support/commands.js
Cypress.Commands.add('login', (email, password) => {
cy.get('input[name="email"]').type(email);
cy.get('input[name="password"]').type(password);
cy.get('button[type="submit"]').click();
});
// 在测试中使用
cy.login('user@example.com', 'password123');
网络请求拦截
javascript
// 拦截 API 请求并模拟响应
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers');
// 拦截 POST 请求并自定义响应
cy.intercept('POST', '/api/login', (req) => {
if (req.body.username === 'validUser') {
req.reply({
statusCode: 200,
body: { token: 'fake-token-123' }
});
} else {
req.reply({
statusCode: 401,
body: { error: 'Invalid credentials' }
});
}
}).as('loginRequest');
// 等待请求完成
cy.wait('@getUsers');
文件上传测试
javascript
cy.fixture('example.json').then((fileContent) => {
cy.get('input[type="file"]').attachFile({
fileContent: JSON.stringify(fileContent),
fileName: 'example.json',
mimeType: 'application/json'
});
});
组件测试
Cypress 10+ 版本支持组件测试:
javascript
// 测试 React 组件
import Button from './Button';
describe('Button 组件', () => {
it('点击时触发回调', () => {
const onClick = cy.stub().as('clickHandler');
cy.mount(<Button onClick={onClick}>点击我</Button>);
cy.get('button').click();
cy.get('@clickHandler').should('have.been.calledOnce');
});
});
配置选项
cypress.config.js
javascript
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1280,
viewportHeight: 720,
defaultCommandTimeout: 5000,
requestTimeout: 10000,
responseTimeout: 10000,
video: false,
screenshotOnRunFailure: true,
chromeWebSecurity: false,
experimentalStudio: true,
retries: {
runMode: 2,
openMode: 0
},
setupNodeEvents(on, config) {
// 注册插件和事件监听器
}
},
component: {
devServer: {
framework: 'react',
bundler: 'webpack'
}
}
});
持续集成
GitHub Actions 集成
yaml
# .github/workflows/cypress.yml
name: Cypress Tests
on: [push, pull_request]
jobs:
cypress-run:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Cypress run
uses: cypress-io/github-action@v5
with:
build: npm run build
start: npm start
wait-on: 'http://localhost:3000'
record: true
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
并行测试
bash
# 在多个机器上并行运行测试
npx cypress run --record --parallel --group "UI tests" --key <record-key>
最佳实践
测试组织
- 使用
describe
和context
对相关测试进行分组 - 使用
beforeEach
设置测试状态,避免测试间依赖 - 按功能或页面组织测试文件
选择器策略
- 使用
data-cy
,data-test
等专用属性作为选择器 - 避免使用类名或 ID,因为它们可能会随着样式变化而改变
- 使用
cy.contains()
查找文本内容
javascript
// 推荐的选择器策略
cy.get('[data-cy=submit-button]');
cy.contains('Submit');
// 不推荐的选择器
cy.get('.btn-primary');
cy.get('#submit');
性能优化
- 使用
cy.session()
缓存登录状态 - 禁用视频录制以加快 CI 运行速度
- 使用
cy.intercept()
模拟慢速 API 响应
常见问题与解决方案
跨域问题
javascript
// cypress.config.js
module.exports = defineConfig({
e2e: {
chromeWebSecurity: false
}
});
等待问题
javascript
// 使用断言等待元素状态变化
cy.get('.loading').should('not.exist');
cy.get('.data-table').should('be.visible');
// 等待特定时间(不推荐,但有时必要)
cy.wait(1000);
认证处理
javascript
// 使用 cy.session 缓存登录状态
cy.session('user-session', () => {
cy.request({
method: 'POST',
url: '/api/login',
body: { username: 'testuser', password: 'password' }
}).then((response) => {
window.localStorage.setItem('token', response.body.token);
});
});
与其他测试工具对比
特性 | Cypress | Selenium | Playwright |
---|---|---|---|
架构 | 浏览器内 | WebDriver | 浏览器自动化 |
语言支持 | JavaScript | 多语言 | 多语言 |
自动等待 | 内置 | 需手动 | 内置 |
调试体验 | 优秀 | 有限 | 良好 |
并行测试 | 支持 | 支持 | 支持 |
浏览器支持 | 主流浏览器 | 全面 | 全面 |
学习曲线 | 平缓 | 陡峭 | 中等 |