5月27日 23:04

Cypress 的测试钩子(before、after、beforeEach、afterEach)如何使用?

Cypress 基于 Mocha 框架,提供了四个测试钩子来控制测试生命周期:beforeafterbeforeEachafterEach。它们让你可以在测试执行前后插入初始化和清理逻辑,避免在每个测试用例中重复编写相同的准备代码。

理解它们的区别很简单——before/after 在整个 describe 块中只跑一次,beforeEach/afterEach 在每个 it 用例前后各跑一次。

四个钩子的执行顺序

先看一段代码,搞清楚它们到底谁先谁后:

javascript
describe('钩子执行顺序', () => { 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')); });

执行日志依次为:

shell
1. 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() 预设后端数据
javascript
describe('商品列表页', () => { 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 执行完之后运行,适合做全局清理。

javascript
describe('用户管理接口测试', () => { 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 用例运行前都有一个干净、一致的初始状态,是测试隔离的核心手段。

javascript
describe('登录表单', () => { 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 用例结束后执行,适合做用例级别的清理或结果校验。

javascript
describe('购物车操作', () => { 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 = 测试用例数量)

维度beforebeforeEach
执行次数1 次每个用例前各 1 次
适合做什么访问页面、建立全局会话重置表单、清空输入、还原状态
状态共享用例间共享 before 的状态每个用例独立,不受前序用例影响
风险某个用例修改了共享状态,后续用例可能受影响更安全,但重复执行会有性能开销

选择建议:如果初始化操作是"只读"的(比如访问页面、读取配置),用 before 就够了。如果涉及"写操作"或者需要确保每个用例的状态独立,用 beforeEach

嵌套 describe 中的钩子继承

describe 嵌套时,内层会继承外层的所有钩子,并且外层钩子先于内层钩子执行:

javascript
describe('外层套件', () => { 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 上的值,这个修改会持续影响后面的用例:

javascript
describe('状态泄漏示例', 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 不执行的边界情况

当测试中途失败或被手动中断时,afterafterEach 可能不会执行。如果你的清理逻辑很关键(比如删除测试数据),不要只放在 after 中,而是在 before 中先做一次清理:

javascript
describe('健壮的清理策略', () => { before(() => { // 先清理上次可能残留的数据,再初始化本次数据 cy.request('DELETE', '/api/test-data'); cy.request('POST', '/api/test-data', { name: 'test' }); }); after(() => { // 正常结束时也清理 cy.request('DELETE', '/api/test-data'); }); });

四个钩子的完整协作示例

把四个钩子放在一起,看看它们在实际项目中如何配合:

javascript
describe('电商下单流程', () => { 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

标签:Cypress