Cypress 默认的设计哲学很干脆:每个测试用例都必须是独立小王国,谁也不靠谁。这意味着 it() 执行前,浏览器上下文会被“一键还原”——页面状态、LocalStorage、Cookies 统统清空,强制维护测试隔离。这一规则让很多新手头疼:明明前一个测试已经创建了员工,后一个测试怎么就没法直接用了?今天我们就来聊聊这个“状态共享”的老问题,以及更聪明的解决思路。
❌ 为什么“携带状态”不是好主意?
- 级联失败风险:假如
it('adds staff')因为 UI 变动或网络异常挂了,那后面所有依赖这个员工状态的测试(搜索、编辑、删除)会像多米诺骨&牌一样全倒,真正的故障点反而被淹没。 - 调试困难:失败后很难追溯——是创建逻辑的问题?还是搜索功能本身有 bug?还是状态传递时出了岔子?一团乱麻。
- 并行/重试失效:Cypress 的
--parallel模式和 retries 机制都要求测试“自治”,一旦状态耦合,并行执行就变得不可预测,重试也无法保证一致结果。 - 违背测试金字塔原则:UI 层的测试应该聚焦于交互验证,而不是承担数据准备的职责。把数据准备工作交给 API 层才是正解。
✅ 推荐方案:通过 API 创建测试数据(最佳实践)
要既保证隔离又满足数据需求,最直接的办法就是绕过 UI,直接调用后端 API 来创建数据。Cypress 提供了 cy.request() 这个利器,完美支持这种模式。来看一个实际的自定义命令封装:
// cypress/support/commands.js
Cypress.Commands.add('createStaffMember', (staffData = {}) => {
const payload = {
name: 'Test Staff',
email: `test-${Date.now()}@example.com`,
role: 'admin',
...staffData
};
cy.request({
method: 'POST',
url: '/api/staff',
body: payload,
headers: {
'Authorization': `Bearer ${Cypress.env('API_TOKEN')}`
}
}).then((response) => {
expect(response.status).to.eq(201);
cy.wrap(response.body.id).as('createdStaffId'); // 可选:供当前测试内后续使用
});
});
然后在测试中直接调用:
describe('Staff Management', () => {
beforeEach(() => {
cy.visit('/staff/list');
});
it('adds a staff member via UI and validates success', () => {
cy.get('[data-testid="add-staff-btn"]').click();
cy.get('[data-testid="name-input"]').type('Test Staff');
cy.get('[data-testid="email-input"]').type(`test-${Date.now()}@example.com`);
cy.get('[data-testid="submit-form"]').click();
cy.get('[data-testid="success-toast"]').should('be.visible');
});
it('displays newly created staff in list (via API setup)', () => {
// ✅ 独立创建:不依赖上一个测试的 UI 操作
cy.createStaffMember({ name: 'API-Created Staff' });
// 刷新页面或触发列表加载
cy.visit('/staff/list');
cy.get('[data-testid="staff-row"]').contains('API-Created Staff').should('be.visible');
});
});
注意:将
cy.createStaffMember()封装为自定义命令并在support/commands.js中注册后,所有测试都可以全局调用它,非常方便。
⚠️ 替代方案(仅限特殊场景)
如果因为技术限制(比如没有可用的 API、权限受限)必须复用 UI 状态,那也请谨慎选择以下方案之一,但千万别当成默认选项。
方案 1:合并为单个测试(推荐度 ★★★☆☆)
把“创建 + 验证”装进同一个 it(),这样逻辑连贯,也容易控制:
it('creates staff and verifies it appears in search', () => {
// Step 1: Create via UI
cy.get('[data-testid="add-staff-btn"]').click();
cy.get('[data-testid="name-input"]').type('Searchable Staff');
cy.get('[data-testid="submit-form"]').click();
cy.get('[data-testid="success-toast"]').should('be.visible');
// Step 2: Search immediately
cy.get('[data-testid="search-input"]').type('Searchable Staff');
cy.get('[data-testid="staff-row"]').should('ha ve.length', 1);
});
方案 2:禁用测试隔离(不推荐,仅作最后手段)
通过 testIsolation: false 关闭隔离(需要 Cypress ≥ 12.15.0),在配置文件中设置:
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
testIsolation: false, // ⚠️ 全局禁用隔离!
},
})
⚠️ 严重警告:这样做会让所有测试共享同一个浏览器上下文,极易引发内存泄漏、状态污染、随机失败等问题。只建议在极短期验证或遗留系统中临时启用,绝对不要用于 CI 环境。
总结
| 方案 | 可维护性 | 调试性 | CI 友好度 | 推荐指数 |
|---|---|---|---|---|
| ✅ API 创建数据 | ★★★★★ | ★★★★★ | ★★★★★ | ⭐⭐⭐⭐⭐ |
| ✅ 合并测试逻辑 | ★★★★☆ | ★★★★☆ | ★★★★☆ | ⭐⭐⭐⭐☆ |
| ⚠️ 禁用测试隔离 | ★★☆☆☆ | ★★☆☆☆ | ★☆☆☆☆ | ⚠️(避免) |
始终优先采用 API 驱动的数据准备——它更快、更稳定、更贴近真实集成场景,也真正践行了 Cypress “测试应反映用户真实行为,而非模拟脆弱路径”的设计哲学。
