Cucumber Tag‑Based Scenario Selection with Hooks: An Architecture Note
Learn how to use Cucumber JVM tags and hooks to run selective tests while keeping setup logic isolated and thread‑safe.
12 Nov 2025, 08:19 UTC

Requirements
Teams often need to run a subset of scenarios (e.g., smoke tests) while keeping the ability to execute the full regression suite. At the same time, common setup and teardown steps (starting a mock service, clearing a database) should be shared without copying code into every step definition.
Smallest Suitable Design
Define tags directly on Feature or Scenario elements. Use Cucumber JVM’s tag expressions to select which scenarios run. Implement @Before and @After hooks that receive the Scenario object and inspect its tag list to decide whether to execute logic.
Example Feature File
# src/test/resources/features/login.feature
Feature: Login functionality
@smoke
Scenario: Successful login with valid credentials
Given the user is on the login page
When they enter valid credentials
Then they are redirected to the dashboard
@regression
Scenario: Login fails with locked account
Given the user is on the login page
When they enter locked‑account credentials
Then an error message is displayed
Hook Implementation
// src/test/java/hooks/EnvHook.java
package hooks;
import io.cucumber.java.After;
import io.cucumber.java.Before;
import io.cucumber.java.Scenario;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EnvHook {
private static final Logger LOG = LoggerFactory.getLogger(EnvHook.class);
@Before
public void setUp(Scenario scenario) {
LOG.info("Starting scenario {} with tags: {}", scenario.getName(), scenario.getSourceTagNames());
if (scenario.getSourceTagNames().contains("@smoke")) {
LOG.info("Applying smoke‑specific setup");
// e.g., start lightweight mock service
}
if (scenario.getSourceTagNames().contains("@regression")) {
LOG.info("Applying regression‑specific setup");
// e.g., seed full test data set
}
}
@After
public void tearDown(Scenario scenario) {
LOG.info("Finishing scenario {} with status: {}", scenario.getName(), scenario.getStatus());
if (scenario.isFailed()) {
LOG.warn("Scenario failed, performing failure‑specific cleanup");
} else {
LOG.debug("Scenario passed, performing normal cleanup");
}
}
}
Trust/Data Boundaries
Hooks execute in the same JVM as step definitions and receive only the Scenario object supplied by Cucumber. They must not rely on static or mutable globals that persist across scenarios, because such state would leak between tests and break isolation. All resources created in a hook (e.g., temporary files, in‑memory databases) should be scoped to the hook’s execution and cleaned up in the corresponding @After block.
Operational Checks
To verify that hooks fire only for the intended tags:
- Run the feature file with a tag filter:
mvn test -Dcucumber.filter.tags="@smoke"(adjust the Maven Surefire configuration as needed). - Inspect the test logs; you should see the
@Beforelog line for the smoke scenario only, and the regression scenario should be skipped. - Repeat with
-Dcucumber.filter.tags="@regression"and confirm the opposite behavior.
To confirm that a hook exception marks the scenario as failed without stopping the suite:
- Introduce a deliberate
throw new RuntimeException("forced failure");inside an@Afterhook. - Execute the full suite (
mvn test). The scenario containing the hook will appear as failed, but subsequent scenarios will still run.
Failure Modes and Design‑Change Conditions
- Hook throws an exception: Cucumber marks the scenario as failed and skips remaining steps. This is expected behavior; however, if the exception occurs in a
@Beforehook, no steps are executed at all. - Asynchronous hook logic: If a hook starts a background thread and does not wait for it to finish, the scenario may complete while the thread is still accessing shared resources, leading to flaky tests. The redesign would involve moving async work into a scoped container or using dependency injection to manage lifecycles.
- Static caches or singletons: Storing state in static fields across scenarios breaks isolation. The fix is to replace static state with objects created per scenario (e.g., via PicoSpring or Guice scopes) and injected into step definitions and hooks.
- Parallel execution with shared mutable state: When using
cucumber-jvm-parallel-plugin, each fork runs its own JVM, but if hooks write to a shared file system location or database without coordination, forks can interfere. Ensure each fork uses a unique temporary directory or database schema, or serialize access through an external lock service.
When the Design Would Need to Change
If the project requires cross‑scenario data sharing that cannot be expressed through tag‑based hook logic (for example, a scenario that needs to verify the outcome of a previous scenario), the current bounded design is insufficient. In that case, consider:
- Introducing a test‑scoped context object passed via dependency injection.
- Using Cucumber’s
@BeforeAlland@AfterAll(available in newer versions) with explicit synchronization. - Adopting a different testing framework that better supports sequential dependencies.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.