什么是 Cypress
巴拉巴拉,不如看图,执行命令之后,就能自己这样了, 
好,如果觉得有点意思,就往下看,然后刚自己项目也来个,一方面能装,一方面能加绩效。
正经文开始。
Cypress 是一个现代化的端到端测试框架,专为现代 Web 应用而设计,其提供了完整的测试解决方案,包括:
- 浏览器测试:直接在浏览器中运行测试,提供真实的用户交互体验
- 时间旅行调试:可以回放测试的每个步骤,查看 DOM 状态、网络请求等
- 自动等待:智能等待元素出现和网络请求完成,减少测试的不稳定性
- 实时重载:修改测试代码时自动重新运行测试
- 丰富的断言:提供多种断言方法,支持 DOM、网络、Cookie 等验证
- 可视化调试:实时查看测试执行过程,快速定位问题
- 测试生成:通过 Cypress Studio 录制用户操作生成测试代码
- 自愈能力 :AI 驱动的智能测试修复和优化
如何使用 Cypress
1. 安装和初始化
bash
# 安装 Cypress
npm install cypress --save-dev
# 或
yarn add cypress --dev
# 或 推荐
pnpm add cypress --dev
# 初始化 Cypress,运行之后项目根目录自动生成cypress
npx cypress open
2. 项目结构
bash
cypress/
├── e2e/ # 端到端测试文件
│ ├── home.cy.js
│ └── user.cy.js
├── fixtures/ # 测试数据文件
│ └── users.json
├── support/ # 支持文件
│ ├── commands.js # 自定义命令
│ └── e2e.js # 全局配置
├── screenshots/ # 失败截图
└── videos/ # 测试视频
3. 基本测试结构
javascript
describe('功能模块测试', () => {
beforeEach(() => {
// 每个测试前的准备工作
cy.visit('/')
})
it('应该能够执行某个操作', () => {
// 测试步骤
cy.get('[data-testid="button"]').click()
cy.contains('成功').should('be.visible')
})
})
常用方法分类
1. 页面导航
javascript
// 访问页面
cy.visit('/login')
cy.visit('https://example.com')
// 页面重载
cy.reload()
cy.reload(true) // 强制重载,忽略缓存
// 前进/后退
cy.go('back')
cy.go('forward')
cy.go(-1) // 后退1步
cy.go(1) // 前进1步
2. 元素查找和操作
javascript
// 查找元素
cy.get('#id') // 通过ID
cy.get('.class') // 通过class
cy.get('[data-testid="button"]') // 通过属性
cy.get('button').contains('登录') // 通过文本内容
// 点击操作
cy.get('button').click()
cy.get('button').click({ force: true }) // 强制点击
cy.get('button').dblclick() // 双击
cy.get('button').rightclick() // 右键点击
// 输入操作
cy.get('input').type('文本内容')
cy.get('input').clear().type('新内容') // 清空后输入
cy.get('input').type('{enter}') // 按回车键
cy.get('input').type('{selectall}{backspace}') // 全选删除
// 选择操作
cy.get('select').select('选项值')
cy.get('input[type="checkbox"]').check()
cy.get('input[type="radio"]').check()
3. 断言验证
javascript
// 可见性断言
cy.get('button').should('be.visible')
cy.get('button').should('not.be.visible')
cy.get('button').should('exist')
cy.get('button').should('not.exist')
// 文本断言
cy.get('h1').should('contain', '标题')
cy.get('h1').should('have.text', '完整标题')
cy.get('input').should('have.value', '输入值')
// 属性断言
cy.get('input').should('have.attr', 'type', 'text')
cy.get('input').should('have.class', 'form-control')
cy.get('input').should('have.css', 'color', 'rgb(0, 0, 0)')
// 数量断言
cy.get('li').should('have.length', 3)
cy.get('li').should('have.length.greaterThan', 2)
cy.get('li').should('have.length.lessThan', 5)
4. 网络请求
javascript
// 监听网络请求
cy.intercept('GET', '/api/users').as('getUsers')
cy.intercept('POST', '/api/login').as('login')
// 等待请求完成
cy.wait('@getUsers')
cy.wait('@login').then((interception) => {
expect(interception.response.statusCode).to.eq(200)
})
// 模拟网络响应
cy.intercept('GET', '/api/users', { fixture: 'users.json' })
cy.intercept('POST', '/api/login', { statusCode: 401, body: { error: 'Unauthorized' } })
5. 文件操作
javascript
// 文件上传
cy.get('input[type="file"]').selectFile('cypress/fixtures/image.jpg')
cy.get('input[type="file"]').selectFile(['file1.txt', 'file2.txt'])
// 文件下载
cy.get('a[download]').click()
cy.readFile('cypress/downloads/document.pdf')
6. 窗口和视口
javascript
// 视口操作
cy.viewport(1280, 720)
cy.viewport('iphone-6')
cy.viewport('macbook-15')
// 窗口操作
cy.window().its('localStorage').should('exist')
cy.window().then((win) => {
win.localStorage.setItem('token', 'abc123')
})
7. Cookie 和存储
javascript
// Cookie 操作
cy.setCookie('sessionId', 'abc123')
cy.getCookie('sessionId').should('have.property', 'value', 'abc123')
cy.clearCookie('sessionId')
cy.clearCookies()
// 本地存储
cy.clearLocalStorage()
cy.getAllLocalStorage()
cy.setLocalStorage('key', 'value')
8. 自定义命令
javascript
// 在 cypress/support/commands.js 中定义
Cypress.Commands.add('login', (username, password) => {
cy.visit('/login')
cy.get('#username').type(username)
cy.get('#password').type(password)
cy.get('button[type="submit"]').click()
})
// 使用自定义命令
cy.login('user@example.com', 'password123')
常用配置
1. 基础配置 (cypress.config.js)
javascript
import { defineConfig } from 'cypress'
export default defineConfig({
// 视口设置
viewportWidth: 1280,
viewportHeight: 720,
// 超时设置
defaultCommandTimeout: 10000,
requestTimeout: 10000,
responseTimeout: 10000,
pageLoadTimeout: 30000,
// 视频和截图
video: true,
screenshotOnRunFailure: true,
videosFolder: 'cypress/videos',
screenshotsFolder: 'cypress/screenshots',
// 浏览器安全
chromeWebSecurity: false,
// E2E 配置
e2e: {
baseUrl: 'http://localhost:3000',
supportFile: 'cypress/support/e2e.js',
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
setupNodeEvents(on, config) {
// 插件配置
on('task', {
log(message) {
console.log(message)
return null
}
})
},
},
// 组件测试配置
component: {
devServer: {
framework: 'create-react-app',
bundler: 'webpack',
},
},
})
2. 环境变量配置
Cypress 支持多种方式设置环境变量,特别适合多套环境的场景:
2.1 在 cypress.config.js 中设置
javascript
// cypress.config.js
export default defineConfig({
env: {
apiUrl: 'https://api.example.com',
username: 'test@example.com',
password: 'password123'
}
})
// 使用环境变量
cy.visit(Cypress.env('apiUrl'))
cy.get('#username').type(Cypress.env('username'))
2.2 使用 cypress.env.json 文件(推荐)
json
// cypress.env.json
{
"testUser": {
"username": "test@example.com",
"password": "password123",
"securityCode": "test123"
},
"baseUrl": "https://test.example.com",
"loginUrl": "https://test.example.com/login",
"apiUrl": "https://api.test.example.com"
}
2.3 多环境配置文件
开发环境配置 (cypress.env.dev.json)
json
{
"testUser": {
"username": "dev@example.com",
"password": "dev123",
"securityCode": "dev123"
},
"baseUrl": "https://dev.example.com",
"loginUrl": "https://dev.example.com/login",
"apiUrl": "https://api.dev.example.com",
"environment": "development"
}
测试环境配置 (cypress.env.test.json)
json
{
"testUser": {
"username": "test@example.com",
"password": "test123",
"securityCode": "test123"
},
"baseUrl": "https://test.example.com",
"loginUrl": "https://test.example.com/login",
"apiUrl": "https://api.test.example.com",
"environment": "testing"
}
生产环境配置 (cypress.env.prod.json)
json
{
"testUser": {
"username": "prod@example.com",
"password": "prod123",
"securityCode": "prod123"
},
"baseUrl": "https://prod.example.com",
"loginUrl": "https://prod.example.com/login",
"apiUrl": "https://api.prod.example.com",
"environment": "production"
}
2.4 动态环境配置
javascript
// cypress.config.js
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// 根据环境变量选择配置文件
const env = config.env.environment || 'test'
const envFile = `cypress.env.${env}.json`
try {
const envConfig = require(`./${envFile}`)
config.env = { ...config.env, ...envConfig }
} catch (error) {
console.log(`环境配置文件 ${envFile} 不存在,使用默认配置`)
}
return config
},
},
})
2.5 命令行环境变量
bash
# 设置环境变量
CYPRESS_baseUrl=https://test.example.com cypress run
# 设置多个环境变量
CYPRESS_baseUrl=https://test.example.com CYPRESS_username=test@example.com cypress run
# 使用环境文件
cypress run --env-file cypress.env.test.json
2.6 package.json 脚本配置
json
{
"scripts": {
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"cypress:dev": "cypress run --env-file cypress.env.dev.json",
"cypress:test": "cypress run --env-file cypress.env.test.json",
"cypress:prod": "cypress run --env-file cypress.env.prod.json",
"cypress:dev:open": "cypress open --env-file cypress.env.dev.json",
"cypress:test:open": "cypress open --env-file cypress.env.test.json"
}
}
2.8 动态环境切换
javascript
// cypress/support/e2e.js
const environments = {
dev: {
baseUrl: 'https://dev.example.com',
apiUrl: 'https://api.dev.example.com',
testUser: {
username: 'dev@example.com',
password: 'dev123'
}
},
test: {
baseUrl: 'https://test.example.com',
apiUrl: 'https://api.test.example.com',
testUser: {
username: 'test@example.com',
password: 'test123'
}
},
prod: {
baseUrl: 'https://prod.example.com',
apiUrl: 'https://api.prod.example.com',
testUser: {
username: 'prod@example.com',
password: 'prod123'
}
}
}
// 根据环境变量选择配置
const currentEnv = Cypress.env('environment') || 'test'
const envConfig = environments[currentEnv]
// 设置全局配置
Cypress.config('baseUrl', envConfig.baseUrl)
Cypress.env('apiUrl', envConfig.apiUrl)
Cypress.env('testUser', envConfig.testUser)
2.9 环境变量优先级
Cypress 环境变量的优先级(从高到低):
- 命令行参数 :
CYPRESS_baseUrl=https://example.com cypress run - cypress.env.json 文件
- cypress.config.js 中的 env 配置
2.10 实际使用示例
javascript
// 在测试中使用环境变量
describe('多环境测试', () => {
beforeEach(() => {
const baseUrl = Cypress.env('baseUrl')
const testUser = Cypress.env('testUser')
console.log(`当前环境: ${Cypress.env('environment')}`)
console.log(`基础URL: ${baseUrl}`)
console.log(`测试用户: ${testUser.username}`)
cy.visit(baseUrl)
})
it('应该能够登录', () => {
const testUser = Cypress.env('testUser')
cy.get('#username').type(testUser.username)
cy.get('#password').type(testUser.password)
cy.get('#securityCode').type(testUser.securityCode)
cy.get('button[type="submit"]').click()
})
})
2.11 环境变量验证
javascript
// cypress/support/e2e.js
// 验证必需的环境变量
beforeEach(() => {
const requiredEnvVars = ['baseUrl', 'testUser']
requiredEnvVars.forEach(envVar => {
if (!Cypress.env(envVar)) {
throw new Error(`缺少必需的环境变量: ${envVar}`)
}
})
// 验证 testUser 对象
const testUser = Cypress.env('testUser')
if (!testUser || !testUser.username || !testUser.password) {
throw new Error('testUser 环境变量配置不完整')
}
})
2.12 CI/CD 环境变量
yaml
# .github/workflows/cypress.yml
name: Cypress Tests
on: [push, pull_request]
jobs:
cypress-run:
runs-on: ubuntu-latest
strategy:
matrix:
environment: [dev, test, prod]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npx cypress run --env-file cypress.env.${{ matrix.environment }}.json
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2.13 环境变量最佳实践
-
敏感信息处理:
javascript// 不要在代码中硬编码敏感信息 // 使用环境变量或加密存储 const apiKey = Cypress.env('apiKey') || 'default-key' -
环境变量文档化:
markdown## 环境变量说明 - `baseUrl`: 应用基础URL - `testUser.username`: 测试用户名 - `testUser.password`: 测试密码 - `apiUrl`: API接口地址 -
环境变量验证:
javascript// 在测试开始前验证环境变量 const validateEnv = () => { const required = ['baseUrl', 'testUser'] required.forEach(key => { if (!Cypress.env(key)) { throw new Error(`Missing required env var: ${key}`) } }) } -
环境变量类型安全:
typescript// cypress/support/index.d.ts declare namespace Cypress { interface Cypress { env(key: 'baseUrl'): string env(key: 'testUser'): { username: string password: string securityCode: string } env(key: 'apiUrl'): string } }
3. 全局配置 (cypress/support/e2e.js)
javascript
// 全局异常处理
Cypress.on('uncaught:exception', (err, runnable) => {
// 忽略特定错误
if (err.message.includes('ResizeObserver loop limit exceeded')) {
return false
}
return true
})
// 全局超时设置
Cypress.config('defaultCommandTimeout', 10000)
Cypress.config('requestTimeout', 10000)
// 全局命令
Cypress.Commands.add('login', (username, password) => {
cy.session([username, password], () => {
cy.visit('/login')
cy.get('#username').type(username)
cy.get('#password').type(password)
cy.get('button[type="submit"]').click()
cy.url().should('not.include', '/login')
})
})
4. 数据驱动测试
javascript
// cypress/fixtures/users.json
{
"users": [
{ "username": "user1", "password": "pass1", "role": "admin" },
{ "username": "user2", "password": "pass2", "role": "user" }
]
}
// 使用测试数据
describe('用户登录测试', () => {
beforeEach(() => {
cy.fixture('users').as('users')
})
it('不同用户登录测试', function() {
this.users.forEach(user => {
cy.login(user.username, user.password)
cy.get('[data-testid="user-role"]').should('contain', user.role)
cy.logout()
})
})
})
5. 并行测试配置
javascript
// cypress.config.js
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// 并行测试配置
if (config.env.CI) {
config.specPattern = [
'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
'cypress/e2e/**/*.spec.{js,jsx,ts,tsx}'
]
}
},
},
})
最佳实践
1. 测试组织
- 使用
describe和it组织测试结构 - 每个测试文件专注于一个功能模块
- 使用
beforeEach和afterEach进行测试准备和清理
2. 元素选择
- 优先使用
data-testid属性 - 避免使用不稳定的 CSS 选择器
- 使用语义化的选择器
3. 等待策略
- 使用
cy.get().should()而不是cy.wait() - 等待元素可见而不是存在
- 使用网络请求等待而不是固定时间
4. 测试数据
- 使用 fixtures 管理测试数据
- 每个测试使用独立的数据
- 测试后清理数据
5. 错误处理
- 使用全局异常处理
- 提供有意义的错误信息
- 使用截图和视频辅助调试
给已有项目加上 cypress 测试
1. 安装 cypress
这里注意,如果是 node 版本在 20 以下需要手动指定 cypress 版本,否则会因为版本不兼容导入失败,最新的 cypress 是 15.15.0。这里我的 node 版本是 18.12.0,所以安装 13.15.0 版本。
bash
pnpm install cypress@13.15.0 -D
pnpm cypress install
2. 配置脚本
这里不加配置的话,自己选择功能和浏览器。如果需要指定功能和浏览器,可以在命令后面加上 --browser 和 --spec 参数,我的项目是后台管理系统,所以指定 e2e 功能和 chrome 浏览器。
在 package.json 中添加以下脚本:
json
"scripts": {
"e2e": "cypress open --e2e --browser chrome"
}
3. 执行脚本,打开 cypress 测试界面
bash
pnpm e2e
执行脚本之后,项目根目录会生成 cypress.config.ts 文件,这个文件是 cypress 的配置文件,可以在这里配置一些全局变量和选项。
js
import { defineConfig } from 'cypress';
export default defineConfig({
// 全局配置 设置浏览器窗口大小、视频录制、截图等
viewportWidth: 1280,
viewportHeight: 1500,
video: true,
screenshotOnRunFailure: true,
defaultCommandTimeout: 10000,
requestTimeout: 10000,
responseTimeout: 10000,
pageLoadTimeout: 30000,
chromeWebSecurity: false,
// 防止会话数据被清除
// experimentalSessionAndOrigin: true,
// E2E 测试配置
e2e: {
baseUrl: 'https://nppss-app-management-test1.test.xdf.cn',
supportFile: 'cypress/support/e2e.js',
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
setupNodeEvents(on, config) {
// 可以在这里添加插件配置
// 例如:on('task', { ... })
},
},
});
在 cypress/support/e2e.js 文件中,可以添加一些全局配置,比如超时时间、异常处理等。
js
// 全局配置
Cypress.on('uncaught:exception', (err, runnable) => {
// 防止未捕获的异常导致测试失败
// 对于 React 应用,这通常是由于开发环境中的热重载导致的
if (err.message.includes('ResizeObserver loop limit exceeded')) {
return false;
}
if (err.message.includes('Non-Error promise rejection captured')) {
return false;
}
// 返回 false 防止 Cypress 失败
return false;
});
// 设置全局超时
Cypress.config('defaultCommandTimeout', 10000);
Cypress.config('requestTimeout', 10000);
Cypress.config('responseTimeout', 10000);
同时,根目录会生成 cypress 目录,这个目录是 cypress 的测试文件目录,可以在这里创建测试文件。
比如创建一个简单的测试文件cypress/e2e/home-page.cy.js:
javascript
describe('简单测试', () => {
it('应该能够访问首页', () => {
cy.visit('https://www.baidu.com');
cy.get('body').should('be.visible');
cy.title().should('not.be.empty');
});
});

4. 拿一个页面试手
拿一个页面试试手,比如教师任务管理页面,cypress/e2e/teacher-task-page.cy.js:

javascript
describe('教师任务管理页面测试', () => {
const pagePath = '/shuangshi-ai/teacher-manage';
// 公共函数:获取iframe内容
const getIframeBody = () => {
// 等待iframe加载完成
cy.get('iframe', { timeout: 30000 }).should('be.visible');
// 等待iframe内容完全加载
cy.get('iframe').should(($iframe) => {
const iframe = $iframe[0];
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
expect(iframeDoc.body).to.exist;
});
return cy
.get('iframe')
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap); // 封装为Cypress可操作对象
};
beforeEach(() => {
// 每个测试前清除存储
cy.clearAllStorage();
// 统一处理页面访问和登录
cy.visit(pagePath);
// 如果被重定向到登录页面,处理登录流程
cy.url().then((url) => {
if (url.includes('/e2/login')) {
console.log('检测到登录重定向,执行登录流程...');
// 等待登录页面加载完成
cy.get('#txtUser').should('be.visible');
cy.get('#txtPwd').should('be.visible');
cy.get('#txtTestEnvSecCode').should('be.visible');
// 输入登录信息
cy.get('#txtUser').type(Cypress.env('testUser').username);
cy.get('#txtPwd').type(Cypress.env('testUser').password);
cy.get('#txtTestEnvSecCode').type(Cypress.env('testUser').securityCode);
// 点击登录按钮
cy.get('#btnLogin').click();
// 等待跳转回目标页面
cy.url({ timeout: 30000 }).should('include', pagePath);
}
});
// 验证页面内容加载
cy.get('body').should('be.visible');
cy.wait(2000); // 等待页面完全加载
});
it('应该能够处理创建功能', () => {
// 监听试卷列表API请求
cy.intercept('GET', '**/npad/teacher/task/paper/list**').as(
'paperListRequest',
);
getIframeBody().within(() => {
// 点击创建按钮
cy.get('.ant-pro-table-list-toolbar-right')
.contains('button', '创建')
.should('be.visible')
.click();
// 验证创建弹框出现
cy.get('.ant-drawer').should('be.visible');
// 选择学校 - 点击选择器,然后选择"上海"
cy.get('.ant-drawer .ant-select').first().should('be.visible').click();
cy.get('.ant-select-dropdown').should('be.visible');
cy.contains('北京').click();
// 选择部门 - 点击选择器,然后选择"智慧学习部"
cy.get('.ant-drawer .ant-select').eq(1).should('be.visible').click();
cy.get('.ant-select-dropdown').should('be.visible');
// 如果已经有默认值,先清除再选择
cy.get('.ant-select-dropdown .ant-select-item')
.contains('智慧学习部')
.click({ force: true });
// 选择学科 - 点击选择器,然后选择"英语"
cy.get('.ant-drawer .ant-select').eq(2).should('be.visible').click();
cy.get('.ant-select-dropdown').should('be.visible');
cy.contains('英语').click({ force: true });
// 输入任务名称
cy.get('.ant-drawer #name')
.should('be.visible')
.clear()
.type('cypress测试任务');
// 选择日期
cy.get('.ant-drawer div[id="type"] label:first-child')
.should('be.visible')
.click();
cy.get('.ant-drawer input[placeholder="开始日期"]')
.should('be.visible')
.click();
cy.get('.ant-picker-panel-container').should('be.visible');
cy.get(
'.ant-picker-panel-container .ant-picker-footer .ant-picker-preset',
)
.eq(0)
.should('be.visible')
.click();
// 去添加试题,打开弹框
cy.wait(1000);
cy.get('.ant-drawer .select-add-container button')
.should('be.visible')
.click();
cy.get('.ant-modal').should('be.visible');
// 定位部门选择器的输入框
cy.get('.ant-modal .ant-select').eq(2).should('be.visible').click();
cy.get('.ant-select-dropdown').should('be.visible');
// 选择本校
cy.contains('本校').click({ force: true });
cy.wait(1000);
// 查询试题
cy.get('.ant-modal-body .ant-pro-table-search .ant-btn-primary')
.scrollIntoView()
.should('be.visible')
.click({ force: true });
// 选择第一个试题
cy.get('.ant-modal-body .ant-radio-wrapper')
.eq(0)
.should('be.visible')
.click();
// 点击确定按钮,选择试题完成
cy.get('.ant-modal-body .footer .confirm-btn')
.should('be.visible')
.click();
// 等待弹框关闭
cy.get('.ant-modal').should('not.exist');
// 点击确定按钮,创建任务完成
cy.get('.ant-drawer .button-box .ant-btn-primary')
.should('be.visible')
.click();
// 关闭弹框,创建任务完成
cy.get('.ant-drawer .ant-drawer-header-title .ant-drawer-close')
.should('be.visible')
.click();
cy.get('.ant-drawer').should('not.exist');
});
});
});