1. 引言

在使用 Cucumber 进行 BDD(行为驱动开发)测试时,我们常常希望在每个场景或步骤前后自动执行一些操作,比如启动浏览器、清理数据、截图等。这些操作本身不属于业务逻辑,但对测试稳定性或调试至关重要。

这时候,Cucumber Hooks 就派上用场了 —— 它允许我们在不修改 Gherkin 脚本的前提下,自动触发预定义的前置和后置动作。

本文将深入讲解 @Before@BeforeStep@AfterStep@After 四大核心 Hook 注解的使用场景、执行顺序以及如何结合标签实现条件执行。


2. Cucumber Hooks 概述

2.1 何时使用 Hooks?

Hooks 适用于执行与业务无关的“幕后”任务,例如:

  • ✅ 启动/关闭浏览器(如 Selenium 测试)
  • ✅ 清理或设置 Cookies / Session
  • ✅ 连接或重置数据库状态
  • ✅ 系统健康检查
  • ✅ 实时监控并上报测试进度(比如推送到 Dashboard)

⚠️ 但要注意:Hooks 不应在 Gherkin 中可见,因此它不能替代 Background 或 Given 步骤。如果你发现某个操作其实是业务流程的一部分(比如“用户已登录”),那它就应该写成 Given,而不是藏在 Hook 里 —— 否则团队成员容易踩坑,调试时一脸懵。

举个典型场景:自动截图。我们希望在每一步前后都截图留证,尤其是失败时便于排查。这种通用能力就很适合用 Hook 实现。

2.2 Hooks 的作用范围

Hooks 默认作用于所有 Scenario 和 Step,具有全局性。

因此最佳实践是:

  • 将所有 Hook 方法集中定义在一个独立的配置类中(比如 HookConfiguration.java
  • ❌ 避免在每个 Step Definition 类里重复定义相同 Hook,否则会导致代码混乱、难以维护

这样既保证了可维护性,也避免了潜在的执行顺序冲突。


3. 核心 Hook 注解详解

3.1 @Before

标记为 @Before 的方法会在每个 Scenario 执行前运行一次

常用于初始化资源,比如启动 WebDriver:

@Before
public void initialization() {
    startBrowser();
}

执行顺序控制

如果存在多个 @Before 方法,可以通过 order 参数指定执行顺序(数值越小优先级越高):

@Before(order = 1)
public void initialization() {
    startBrowser();
}

@Before(order = 2)
public void beforeScenario() {
    takeScreenshot();
}

上述代码中,initialization() 先执行,接着才是 beforeScenario()

⚠️ 注意:默认 order 值为 10000,所以不指定时会最后执行。


3.2 @BeforeStep

该方法在每个 Step 执行前运行,适合做细粒度的准备操作,比如:

  • 每步前截图
  • 日志打点
  • 性能埋点

示例:

@BeforeStep
public void beforeStep() {
    takeScreenshot();
}

每当你看到 Gherkin 中的一行 WhenThen,这个方法都会先被触发。


3.3 @AfterStep

@BeforeStep 对应,在每个 Step 执行后运行,无论该 Step 成功还是失败。

非常适合用于:

  • 捕获失败瞬间的截图
  • 记录每步耗时
  • 自动重试前的状态保存
@AfterStep
public void afterStep() {
    takeScreenshot();
}

✅ 关键特性:即使 Step 失败,@AfterStep 依然会执行 —— 这点非常有用,能确保关键诊断信息不会丢失。


3.4 @After

每个 Scenario 执行结束后运行,常用于资源释放:

@After
public void afterScenario() {
    takeScreenshot();  // 最终状态截图
    closeBrowser();    // 关闭浏览器
}

✅ 与 @AfterStep 类似,无论 Scenario 成功或失败,@After 都会执行,行为类似于 Java 中的 finally 块,非常适合做清理工作。


3.5 接收 Scenario 参数

所有 Hook 方法都可以接收一个 io.cucumber.java.Scenario 类型的参数,用于获取当前场景的运行时信息:

@After
public void afterScenario(Scenario scenario) {
    if (scenario.isFailed()) {
        byte[] screenshot = takeScreenshotAsBytes();
        scenario.attach(screenshot, "image/png", "failure-screenshot");
    }
    closeBrowser();
}

通过 Scenario 对象可以获取:

  • 场景名称(getName()
  • 当前状态(isFailed()
  • 步骤列表与执行结果
  • 附加附件(attach(...))—— 支持图片、日志等二进制数据嵌入报告

这在生成富文本测试报告时非常实用。


4. Hook 执行顺序详解

4.1 正常流程(Happy Flow)

考虑以下 Feature 文件:

Feature: Book Store With Hooks
  Background: The Book Store
    Given The following books are available in the store
      | The Devil in the White City          | Erik Larson |
      | The Lion, the Witch and the Wardrobe | C.S. Lewis  |
      | In the Garden of Beasts              | Erik Larson |

  Scenario: 1 - Find books by author
    When I ask for a book by the author Erik Larson
    Then The salesperson says that there are 2 books

  Scenario: 2 - Find books by author, but isn't there
    When I ask for a book by the author Marcel Proust
    Then The salesperson says that there are 0 books

当运行该测试时,Hook 的执行顺序如下图所示:

Screenshot-2019-12-29-at-15.28.42

完整流程为:

  1. @Before(按 order 排序)→
  2. 每个 Step:
    • @BeforeStep
    • Step 执行
    • @AfterStep
  3. @After

✅ 所有 Hook 对每个 Scenario 独立执行一遍。


4.2 异常流程:Step 失败

如果某个 Step 执行失败,流程如下:

Screenshot-2019-12-29-at-13.32.59

关键点:

  • @Before 已执行(资源已初始化)
  • ❌ 后续 Step 被跳过
  • ✅ 当前 Step 的 @AfterStep 仍会执行
  • ✅ 最终 @After 一定会执行(相当于 finally)

📌 这意味着你可以放心在 @After 中关闭资源,不必担心因异常导致资源泄漏。


4.3 异常流程:Hook 自身失败

更极端的情况是 Hook 自己抛出异常。例如第一个 @BeforeStep 失败:

Screenshot-2019-12-29-at-13.30.49

行为表现:

  • ❌ 对应的 Step 不会执行
  • ✅ 该 Step 的 @AfterStep 仍然执行
  • ❌ 后续 Step 全部跳过
  • @After 依然执行

⚠️ 提示:Hook 抛异常会导致整个 Scenario 标记为失败,所以务必确保 Hook 本身的健壮性,尤其是截图、日志这类“辅助功能”。


5. 使用 Tags 实现条件执行

虽然 Hooks 默认全局生效,但我们可以通过 Cucumber Tags 控制其执行范围,实现更灵活的策略。

语法如下:

@Before(order = 2, value = "@Screenshots")
public void beforeScenario() {
    takeScreenshot();
}

此时该 Hook 仅对带有 @Screenshots 标签的 Scenario 生效

@Screenshots
Scenario: 1 - Find books by author 
  When I ask for a book by the author Erik Larson 
  Then The salesperson says that there are 2 books

常见应用场景:

  • @Mobile:仅对移动端测试启动 Appium
  • @DatabaseTest:只在涉及数据库的场景中重置数据
  • @Debug:开启详细日志或截图

✅ 利用 Tags + Hook,可以做到“按需加载”,避免不必要的性能开销。


6. Java 8 Lambda 风格写法

从 Cucumber JVM 4.0 开始,支持使用 Java 8 的 Lambda 表达式定义 Hooks,特别适合轻量级逻辑。

传统写法:

@Before(order = 2)
public void initialization() {
    startBrowser();
}

Lambda 写法:

public BookStoreWithHooksRunSteps() {
    Before(2, () -> startBrowser());
}

同样适用于其他 Hook:

BeforeStep(() -> takeScreenshot());
AfterStep(() -> System.out.println("Step completed"));
After(scenario -> {
    if (scenario.isFailed()) {
        scenario.attach(takeScreenshotAsBytes(), "image/png", "error");
    }
    closeBrowser();
});

⚠️ 注意:Lambda 写法需在构造函数中调用静态方法 BeforeAfterStep 等,且类需实现 EnBefore 等接口(通常是继承 AbstractTestNGCucumberTests 或手动注册)。

优点是代码更简洁;缺点是调试不如普通方法直观,适合小型项目或偏好函数式风格的团队。


7. 总结

Cucumber Hooks 是提升自动化测试健壮性和可观测性的利器。本文重点归纳如下:

Hook 触发时机 是否处理失败 常见用途
@Before 每个 Scenario 前 初始化浏览器、数据准备
@BeforeStep 每个 Step 前 截图、打点、上下文准备
@AfterStep 每个 Step 后 失败截图、性能记录
@After 每个 Scenario 后 资源释放、最终截图、清理

📌 关键建议:

  • ✅ 将 Hooks 集中管理,避免散落在多个类中
  • ✅ 善用 Scenario 参数做差异化处理(如失败才截图)
  • ✅ 结合 Tags 实现按需执行,避免过度自动化
  • ❌ 不要用 Hook 替代业务步骤,保持 Gherkin 可读性

示例代码已托管至 GitHub:https://github.com/baomidou/tutorials/tree/master/testing-cucumber-hooks

合理使用 Hooks,能让你的自动化测试既强大又清晰,真正成为团队的“质量守门员”。


原始标题:Cucumber Hooks