Cypress 前端测试 — 在真实浏览器中运行的端到端与组件测试框架¶
一句话说明¶
Cypress 是一个完全运行在浏览器内部的测试框架,测试代码和应用代码在同一个进程中执行,调试直观(能看到每一步的 DOM 快照),支持 E2E 测试和组件测试,当前版本 v13.x(2025年稳定)。
安装与配置¶
# 安装 Cypress
npm install -D cypress
# 首次打开(自动创建目录结构)
npx cypress open
# 直接运行测试(无头模式,适合 CI)
npx cypress run
# 查看版本
npx cypress --version
// cypress.config.js — 配置文件
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:5173', // 应用基础 URL
specPattern: 'cypress/e2e/**/*.cy.ts', // 测试文件匹配规则
supportFile: 'cypress/support/e2e.ts', // 支持文件路径
viewportWidth: 1280, // 浏览器宽度
viewportHeight: 720, // 浏览器高度
video: false, // 是否录制视频(CI 建议关闭节省空间)
screenshotOnRunFailure: true, // 失败时自动截图
defaultCommandTimeout: 10000, // 命令超时时间(ms)
retries: {
runMode: 2, // CI 运行时失败重试2次
openMode: 0, // 开发时不重试
},
},
component: {
devServer: {
framework: 'react', // 组件测试用 React
bundler: 'vite', // 打包器用 Vite
},
},
});
核心用法¶
// cypress/e2e/login.cy.ts
describe('登录功能', () => { // describe 分组
beforeEach(() => { // 每个测试前执行
cy.visit('/login'); // 打开登录页
});
it('成功登录后跳转 dashboard', () => {
// ── 操作元素 ──
cy.get('[name=username]').type('testuser'); // 输入用户名
cy.get('[name=password]').type('password'); // 输入密码
cy.get('button[type=submit]').click(); // 点击提交
// ── 断言(Cypress 自动重试直到通过或超时)──
cy.url().should('include', '/dashboard'); // URL 应包含 /dashboard
cy.contains('h1', '欢迎回来').should('be.visible'); // 标题可见
});
it('密码错误时显示错误提示', () => {
cy.get('[name=username]').type('testuser');
cy.get('[name=password]').type('wrong');
cy.get('button[type=submit]').click();
cy.get('.error-message') // 错误提示元素
.should('be.visible') // 应该可见
.and('contain', '用户名或密码错误'); // 应该包含此文字
});
it('使用 Fixture 数据测试', () => {
cy.fixture('user.json').then(user => { // 读取 fixtures/user.json
cy.get('[name=username]').type(user.name); // 使用 fixture 中的数据
cy.get('[name=password]').type(user.password);
});
});
});
实战案例¶
// ── 测试生信工具的核心流程 ──
// cypress/e2e/blast-search.cy.ts
describe('BLAST 序列比对', () => {
beforeEach(() => {
// 拦截 API 请求,避免真实调用(加快测试速度)
cy.intercept('POST', '/api/blast', {
statusCode: 200,
body: {
hits: [
{ accession: 'NM_000546', score: 98.5, evalue: '1e-120', description: 'TP53' },
{ accession: 'NM_007294', score: 95.1, evalue: '2e-98', description: 'BRCA1' },
],
queryLength: 500,
},
}).as('blastRequest'); // 给拦截起个别名
cy.visit('/blast'); // 打开 BLAST 页面
});
it('提交序列并显示比对结果', () => {
const testSeq = 'ATGCGATCGTAGCTAGCTAGCTAG'; // 测试序列
// 输入序列
cy.get('[data-cy=sequence-input]').type(testSeq);
// 选择数据库
cy.get('[data-cy=database-select]').select('nr'); // 选择 nr 数据库
// 点击运行
cy.get('[data-cy=run-blast]').click();
// 等待 API 请求完成
cy.wait('@blastRequest'); // 等待别名为 blastRequest 的请求
// 验证结果表格
cy.get('[data-cy=result-table] tbody tr') // 表格行
.should('have.length', 2); // 应有2行结果
cy.get('[data-cy=result-table]')
.contains('td', 'TP53') // 表格中应包含 TP53
.should('be.visible');
});
it('验证 E-value 过滤功能', () => {
cy.get('[data-cy=evalue-threshold]').clear().type('1e-100'); // 设置阈值
cy.get('[data-cy=apply-filter]').click();
// 只有 evalue < 1e-100 的结果(TP53)应显示
cy.get('[data-cy=result-table] tbody tr').should('have.length', 1);
});
});
// cypress/support/commands.ts — 自定义命令
Cypress.Commands.add('login', (username: string, password: string) => {
cy.session([username, password], () => { // session 缓存登录状态
cy.visit('/login');
cy.get('[name=username]').type(username);
cy.get('[name=password]').type(password);
cy.get('button[type=submit]').click();
cy.url().should('include', '/dashboard');
});
});
// 使用自定义命令
cy.login('admin', 'password'); // 一行完成登录
常见报错与解决¶
| 报错 | 原因 | 解决方法 |
|---|---|---|
cy.get() timed out | 元素不存在或未出现 | 检查选择器,增加 { timeout: 15000 } |
cy.visit() failed | 应用未启动 | 确认 baseUrl 服务已运行 |
CORS error | 跨域请求未处理 | 在 cypress.config 中设置 chromeWebSecurity: false |
| 测试间状态污染 | beforeEach 未清理状态 | 在 beforeEach 中重置数据或清除 cookies |
| CI 中视频录制慢 | 视频编码消耗 CPU | 设置 video: false 关闭录制 |
速查表¶
# 运行命令
npx cypress open # 开启 Cypress 图形界面(开发调试用)
npx cypress run # 无头运行(CI 用)
npx cypress run --spec "cypress/e2e/login.cy.ts" # 只运行指定文件
npx cypress run --browser firefox # 指定浏览器
# 常用 Cypress 命令
cy.visit(url) # 打开页面
cy.get(selector) # 选择元素
cy.contains(text) # 按文字查找元素
cy.click() # 点击
cy.type(text) # 输入文字
cy.should(assertion) # 断言
cy.wait(alias) # 等待 API 请求
cy.intercept(method, url, response) # 拦截请求
cy.fixture(filename) # 读取测试数据
cy.screenshot() # 手动截图
# 官方文档:https://docs.cypress.io/
# vs Playwright 对比:Cypress 调试更直观,Playwright 多浏览器更强