Skip to content

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>

最佳实践

测试组织

  • 使用 describecontext 对相关测试进行分组
  • 使用 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);
  });
});

与其他测试工具对比

特性CypressSeleniumPlaywright
架构浏览器内WebDriver浏览器自动化
语言支持JavaScript多语言多语言
自动等待内置需手动内置
调试体验优秀有限良好
并行测试支持支持支持
浏览器支持主流浏览器全面全面
学习曲线平缓陡峭中等

学习资源