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 中的一行 When
或 Then
,这个方法都会先被触发。
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 的执行顺序如下图所示:
完整流程为:
@Before
(按 order 排序)→- 每个 Step:
@BeforeStep
- Step 执行
@AfterStep
@After
✅ 所有 Hook 对每个 Scenario 独立执行一遍。
4.2 异常流程:Step 失败
如果某个 Step 执行失败,流程如下:
关键点:
- ✅
@Before
已执行(资源已初始化) - ❌ 后续 Step 被跳过
- ✅ 当前 Step 的
@AfterStep
仍会执行 - ✅ 最终
@After
一定会执行(相当于 finally)
📌 这意味着你可以放心在 @After
中关闭资源,不必担心因异常导致资源泄漏。
4.3 异常流程:Hook 自身失败
更极端的情况是 Hook 自己抛出异常。例如第一个 @BeforeStep
失败:
行为表现:
- ❌ 对应的 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 写法需在构造函数中调用静态方法 Before
、AfterStep
等,且类需实现 En
、Before
等接口(通常是继承 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,能让你的自动化测试既强大又清晰,真正成为团队的“质量守门员”。