Cucumber Hooks — Before, After, BeforeStep, AfterStep

8 min read

Step definitions handle what happens during a step. Hooks handle what happens around a scenario — the browser startup, the auth token, the screenshot on failure, the database cleanup. Without hooks, every step definition class would duplicate lifecycle code. With hooks, you write it once and every scenario benefits.

The four hook types

Cucumber provides four annotations, all in io.cucumber.java:

import io.cucumber.java.Before;
import io.cucumber.java.After;
import io.cucumber.java.BeforeStep;
import io.cucumber.java.AfterStep;
import io.cucumber.java.Scenario;
 
public class Hooks {
 
    @Before
    public void beforeScenario(Scenario scenario) {
        System.out.println("Starting scenario: " + scenario.getName());
    }
 
    @After
    public void afterScenario(Scenario scenario) {
        System.out.println("Finished: " + scenario.getStatus());
    }
 
    @BeforeStep
    public void beforeStep(Scenario scenario) {
        // runs before every individual step
    }
 
    @AfterStep
    public void afterStep(Scenario scenario) {
        // runs after every individual step
    }
}

@Before and @After fire once per scenario. @BeforeStep and @AfterStep fire once per step within each scenario. For a scenario with 5 steps, @Before fires once, @BeforeStep and @AfterStep each fire 5 times, @After fires once.

The Scenario parameter is optional — declare it when you need access to scenario metadata (name, tags, status). Leave it out when you don't.

The Scenario object

Scenario gives you access to everything about the currently running scenario:

scenario.getName()       // "Successful login with valid credentials"
scenario.getId()         // unique ID string
scenario.getStatus()     // PASSED, FAILED, PENDING, SKIPPED, AMBIGUOUS
scenario.isFailed()      // convenience boolean
scenario.getSourceTagNames()  // ["@smoke", "@critical"]
 
// Attach data to the report
scenario.attach(bytes, "image/png", "screenshot");
scenario.attach("Error log here", "text/plain", "log");
scenario.log("Custom message visible in the report");

The most common use: attach a screenshot to the report when a scenario fails.

Screenshot on failure — the universal After hook

public class Hooks {
    private final TestContext context;
 
    public Hooks(TestContext context) {
        this.context = context;
    }
 
    @After
    public void afterScenario(Scenario scenario) {
        WebDriver driver = context.getDriver();
        if (scenario.isFailed() && driver != null) {
            byte[] screenshot = ((TakesScreenshot) driver)
                .getScreenshotAs(OutputType.BYTES);
            scenario.attach(screenshot, "image/png", scenario.getName());
        }
        context.quitDriver();
    }
}

This single @After hook covers every UI scenario in the suite. scenario.isFailed() is only true after the last step has run, so you always get the final state of the page. scenario.attach(...) embeds the image directly in the HTML report and in Allure.

Tagged hooks: scoped to specific scenarios

Passing a tag expression to the annotation means the hook only fires for matching scenarios:

@Before("@ui")
public void setupBrowser() {
    WebDriverManager.chromedriver().setup();
    context.setDriver(new ChromeDriver());
    context.getDriver().manage().window().maximize();
}
 
@After("@ui")
public void closeBrowser() {
    context.quitDriver();
}
 
@Before("@api")
public void configureApiClient() {
    RestAssured.baseURI = System.getProperty("apiBaseUrl", "https://api.staging.myapp.com");
    RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}

@ui scenarios trigger browser setup; @api scenarios trigger API configuration. A scenario tagged @ui and @api triggers both. A scenario with neither tag triggers neither. This keeps hook overhead proportional to what each scenario actually needs.

Full tag expressions work: @Before("@ui and not @mock"), @After("@smoke or @critical").

Hook ordering

When multiple @Before or @After hooks exist, control their order with the order attribute:

@Before(order = 1)   // runs first (lower number = earlier)
public void startTestContext() { ... }
 
@Before(order = 2)
public void setupBrowser() { ... }
 
@Before(order = 3)
public void loadTestData() { ... }
 
@After(order = 3)    // @After runs in reverse order: highest first
public void cleanupTestData() { ... }
 
@After(order = 2)
public void closeBrowser() { ... }
 
@After(order = 1)
public void tearDownContext() { ... }

@Before hooks run lowest-order-first (setup order: 1 → 2 → 3). @After hooks run highest-order-first (teardown order: 3 → 2 → 1). This mirrors the natural setup/teardown nesting: what you set up last, you tear down first. The default order is 10000 — hooks without an explicit order run after all ordered hooks.

The critical import distinction

Cucumber's hooks are not JUnit or TestNG annotations. A common error: writing @Before and importing org.junit.Before instead of io.cucumber.java.Before. The JUnit annotation is picked up as a test method by the runner — the method runs but not as a hook, and Cucumber ignores it entirely.

Always import from io.cucumber.java:

import io.cucumber.java.Before;   // ✅ Cucumber hook
import io.cucumber.java.After;    // ✅ Cucumber hook
 
// NOT:
import org.junit.Before;          // ❌ JUnit lifecycle annotation
import org.junit.After;           // ❌ JUnit lifecycle annotation
import org.testng.annotations.BeforeMethod;  // ❌ TestNG annotation

IntelliJ's auto-import will suggest both — always check the package in the import statement.

The hook lifecycle, visualised

⚠️ Common mistakes

  • Wrong @Before/@After import. Importing JUnit's @Before instead of Cucumber's is silent — no compile error, no runtime warning. The method just doesn't behave as a Cucumber hook. Always check the full import path.
  • Creating the driver in @Before but quitting in the step definition's @Then. If a scenario fails before reaching @Then, the driver leaks. Teardown belongs in @After, which runs even on failure.
  • Using @BeforeStep for business logic. @BeforeStep fires before every step — adding logging or debugging there multiplies the overhead by the number of steps in every scenario. Use it for cross-cutting concerns only (e.g., logging step names during debugging), and remove it from production hooks.
  • Relying on @After hook order across different classes without order. When multiple @After hooks exist in different classes, their relative order without explicit order values is undefined. Always set order when teardown sequence matters.

🎯 Practice task

Build a complete hooks class for a UI scenario suite. 35 minutes.

  1. Create src/test/java/hooks/Hooks.java. Add @Before("@ui") that creates a ChromeDriver and stores it in a TestContext (create a minimal TestContext class with a driver field). Add @After("@ui") that takes a screenshot on failure, attaches it to the Scenario, and quits the driver.
  2. Tag your existing login scenario @ui and run it. Verify the driver lifecycle logs appear.
  3. Deliberately fail the scenario (wrong URL or wrong assertion). Run again — open target/cucumber-reports.html and confirm the screenshot is embedded in the failed scenario entry.
  4. Add @Before("@api") that sets RestAssured.baseURI to any URL. Tag a separate scenario @api. Confirm only the API hook fires for that scenario and the browser hook does not.
  5. Stretch: add two @Before hooks with explicit order = 1 and order = 2. Print a message from each. Confirm order 1 always runs first in the console output.

Next lesson: sharing the driver and test state across multiple step definition classes using dependency injection.

// tip to track lessons you complete and pick up where you left off across devices.