Cypress 的测试钩子(before、after、beforeEach、afterEach)如何使用?
Cypress 基于 Mocha 框架,提供了四个测试钩子来控制测试生命周期:before、after、beforeEach 和 afterEach。它们让你可以在测试执行前后插入初始化和清理逻辑,避免在每个测试用例中重复编写相同的准备代码。
理解它们的区别很简单——before/after 在整个 describe 块中只跑一次,beforeEach/afterEach 在每个 it 用例前后各跑一次。
四个钩子的执行顺序
先看一段代码,搞清楚它们到底谁先谁后:
javascriptdescribe('钩子执行顺序', () => { before(() => cy.log('1. before')); beforeEach(() => cy.log('2. beforeEach')); afterEach(() => cy.log('4. afterEach')); after(() => cy.log('5. after')); it('测试用例 A', () => cy.log('3. it A')); it('测试用例 B', () => cy.log('3. it B')); });
执行日志依次为:
shell1. before ← 整个套件开始前,执行一次 2. beforeEach ← 用例 A 前 3. it A 4. afterEach ← 用例 A 后 2. beforeEach ← 用例 B 前 3. it B 4. afterEach ← 用例 B 后 5. after ← 整个套件结束后,执行一次
记住这条链路:before → (beforeEach → it → afterEach) × N → after,所有关于钩子的问题都能用这条链路解释。
before:整个套件只执行一次的前置操作
before 适合做那些"只需要做一次"的初始化工作。
典型场景:
- 访问被测页面
cy.visit('/login') - 用
cy.session()建立全局登录态 - 通过
cy.request()预设后端数据
javascriptdescribe('商品列表页', () => { before(() => { // 一次性访问目标页面 cy.visit('/products'); }); it('页面标题正确', () => { cy.get('h1').should('contain', '商品列表'); }); it('列表不为空', () => { cy.get('.product-item').should('have.length.gt', 0); }); });
关键注意点:before 中通过 cy.visit() 访问页面后,Cypress 会保持该页面状态,后续用例不需要再次访问。但如果某个用例导航到了别的页面,就需要在 beforeEach 中重新访问。
after:整个套件只执行一次的收尾操作
after 在所有测试用例和所有 afterEach 执行完之后运行,适合做全局清理。
javascriptdescribe('用户管理接口测试', () => { before(() => { // 创建测试用户 cy.request('POST', '/api/users', { name: 'test_user', email: 'test@example.com' }); }); after(() => { // 清理:删除测试过程中创建的用户 cy.request('DELETE', '/api/users/test@example.com'); }); it('用户可以被查询到', () => { cy.request('/api/users/test@example.com') .its('status') .should('eq', 200); }); });
注意:Cypress 官方建议谨慎依赖 after 做状态清理。如果测试中途失败,after 中的清理逻辑可能不会执行,残留数据会影响下次运行。更稳妥的做法是在 before 中先清理再初始化,确保每次测试都从干净状态开始。
beforeEach:每个用例前的状态重置
beforeEach 是使用频率最高的钩子。它保证每个 it 用例运行前都有一个干净、一致的初始状态,是测试隔离的核心手段。
javascriptdescribe('登录表单', () => { beforeEach(() => { // 每个用例前都重新访问登录页 cy.visit('/login'); }); it('空字段提交时显示错误提示', () => { cy.get('button[type="submit"]').click(); cy.get('.error-msg').should('be.visible'); }); it('输入正确凭据后跳转到首页', () => { cy.get('#username').type('admin'); cy.get('#password').type('secret'); cy.get('button[type="submit"]').click(); cy.url().should('include', '/dashboard'); }); it('密码错误时显示错误提示', () => { cy.get('#username').type('admin'); cy.get('#password').type('wrong'); cy.get('button[type="submit"]').click(); cy.get('.error-msg').should('contain', '密码不正确'); }); });
beforeEach 最大的价值是测试隔离——无论前一个用例做了什么操作(比如输入了错误密码),下一个用例都会从全新的登录页开始,互不干扰。
afterEach:每个用例后的清理
afterEach 在每个 it 用例结束后执行,适合做用例级别的清理或结果校验。
javascriptdescribe('购物车操作', () => { beforeEach(() => { cy.visit('/cart'); }); afterEach(() => { // 每个用例后清空 localStorage,防止状态残留 cy.clearLocalStorage(); // 每个用例后截图,方便排查失败原因 cy.screenshot(); }); it('添加商品后数量更新', () => { cy.get('.add-btn').first().click(); cy.get('#cart-count').should('eq', '1'); }); it('删除商品后列表清空', () => { cy.get('.add-btn').first().click(); cy.get('.remove-btn').first().click(); cy.get('.cart-item').should('not.exist'); }); });
before vs beforeEach:什么时候用哪个?
这是新手最常混淆的问题,核心区别就一句话:before 执行一次,beforeEach 执行 N 次(N = 测试用例数量)。
| 维度 | before | beforeEach |
|---|---|---|
| 执行次数 | 1 次 | 每个用例前各 1 次 |
| 适合做什么 | 访问页面、建立全局会话 | 重置表单、清空输入、还原状态 |
| 状态共享 | 用例间共享 before 的状态 | 每个用例独立,不受前序用例影响 |
| 风险 | 某个用例修改了共享状态,后续用例可能受影响 | 更安全,但重复执行会有性能开销 |
选择建议:如果初始化操作是"只读"的(比如访问页面、读取配置),用 before 就够了。如果涉及"写操作"或者需要确保每个用例的状态独立,用 beforeEach。
嵌套 describe 中的钩子继承
当 describe 嵌套时,内层会继承外层的所有钩子,并且外层钩子先于内层钩子执行:
javascriptdescribe('外层套件', () => { before(() => cy.log('外层 before')); beforeEach(() => cy.log('外层 beforeEach')); describe('内层套件', () => { before(() => cy.log('内层 before')); beforeEach(() => cy.log('内层 beforeEach')); it('用例 1', () => cy.log('用例 1 执行')); }); });
执行顺序:
shell外层 before → 内层 before → 外层 beforeEach → 内层 beforeEach → 用例 1
注意:before 在嵌套场景下的行为容易踩坑——内层的 before 并非"在内层用例前"执行,而是在整个嵌套套件的 before 阶段一起执行。如果内层有多个用例,内层 before 也只执行一次,不是每个内层用例前都执行。
常见问题与踩坑
1. before 中的状态泄漏
Cypress 中 before 里用 this 赋值的变量,后续用例可以通过 this 访问。但如果你在某个用例中修改了 this 上的值,这个修改会持续影响后面的用例:
javascriptdescribe('状态泄漏示例', function() { before(function() { this.count = 0; }); it('用例 A', function() { this.count = 10; // 修改了 this.count expect(this.count).to.eq(10); }); it('用例 B', function() { // this.count 已经被用例 A 修改为 10,不是初始的 0 expect(this.count).to.eq(10); // 这会通过,但不一定是你预期的 }); });
解决方案:用 beforeEach 重置状态,或者用闭包变量(const/let)代替 this,避免状态在用例间泄漏。
2. 钩子中混用 async/await
Cypress 的 cy 命令不是 Promise,不能直接用 await。在钩子中如果需要等待 cy 命令完成,直接链式调用即可,不要加 async:
javascript// ❌ 错误:cy.visit 不会被 await 正确等待 before(async () => { await cy.visit('/login'); }); // ✅ 正确:直接链式调用 before(() => { cy.visit('/login'); });
3. after/afterEach 不执行的边界情况
当测试中途失败或被手动中断时,after 和 afterEach 可能不会执行。如果你的清理逻辑很关键(比如删除测试数据),不要只放在 after 中,而是在 before 中先做一次清理:
javascriptdescribe('健壮的清理策略', () => { before(() => { // 先清理上次可能残留的数据,再初始化本次数据 cy.request('DELETE', '/api/test-data'); cy.request('POST', '/api/test-data', { name: 'test' }); }); after(() => { // 正常结束时也清理 cy.request('DELETE', '/api/test-data'); }); });
四个钩子的完整协作示例
把四个钩子放在一起,看看它们在实际项目中如何配合:
javascriptdescribe('电商下单流程', () => { before(() => { // 全局初始化:创建测试商品 cy.request('POST', '/api/products', { id: 'prod_001', name: '测试商品', price: 99.9 }); // 访问商品页 cy.visit('/products/prod_001'); }); beforeEach(() => { // 每个用例前:确保在正确的页面上 cy.visit('/products/prod_001'); // 清空购物车 cy.request('DELETE', '/api/cart'); }); afterEach(() => { // 每个用例后:截图留档 cy.screenshot(); }); after(() => { // 全局清理:删除测试商品 cy.request('DELETE', '/api/products/prod_001'); }); it('添加商品到购物车', () => { cy.get('.add-to-cart').click(); cy.get('#cart-count').should('contain', '1'); }); it('修改商品数量', () => { cy.get('.add-to-cart').click(); cy.get('.quantity-input').clear().type('3'); cy.get('.cart-total').should('contain', '299.7'); }); it('删除购物车商品', () => { cy.get('.add-to-cart').click(); cy.get('.remove-item').click(); cy.get('#cart-count').should('contain', '0'); }); });
掌握这四个钩子的执行时机和使用场景,是写出干净、可靠、可维护的 Cypress 测试的基础。核心原则就两条:需要共享的只做一次用 before/after,需要隔离的每次都做用 beforeEach/afterEach。