ScreenshotNeo

BlogHow-to

How to Capture a Screenshot After Each Cucumber Step with Java and TestNG

Use Cucumber’s @AfterStep with Selenium to attach PNGs after every executed Java step, including TestNG setup, parallel safety, fixes, and ScreenshotNeo.

By the ScreenshotNeo team1 October 20266 min read

Direct answer: add a Cucumber-JVM @AfterStep hook that receives the current Scenario, obtains PNG bytes from the same Selenium WebDriver used by your step definitions, and calls scenario.attach(...). The hook runs after every step that actually executes. If a step fails, Cucumber skips later steps and their hooks, so no hook can capture steps that were never run.

1. What the hook does

Cucumber step hooks have invoke-around behavior: @AfterStep runs after each executed step. Selenium’s TakesScreenshot#getScreenshotAs(OutputType.BYTES) returns PNG bytes, and Scenario.attach(byte[], mediaType, name) embeds those bytes in the report.

Event Screenshot result
Step passes The hook captures and attaches a PNG.
Step fails The hook still runs for that failed step, so its final browser state is attached.
Later step after a failure That step and its hooks are skipped.
Scenario setup fails before a step No after-step hook runs because no step executed.

2. Add the hook to a Java and TestNG project

2.1 Keep one driver in a test context

The hook must use the same driver instance as the step definitions. A small context object makes that dependency explicit and works with a dependency-injection object factory.

package steps;

import org.openqa.selenium.WebDriver;

public final class TestContext {
    private final WebDriver driver;

    public TestContext(WebDriver driver) {
        this.driver = driver;
    }

    public WebDriver driver() {
        return driver;
    }
}

2.2 Capture after every executed step

package steps;

import io.cucumber.java.AfterStep;
import io.cucumber.java.Scenario;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;

public class ScreenshotHooks {
    private final WebDriver driver;

    public ScreenshotHooks(TestContext context) {
        this.driver = context.driver();
    }

    @AfterStep
    public void captureAfterStep(Scenario scenario) {
        byte[] png = ((TakesScreenshot) driver)
                .getScreenshotAs(OutputType.BYTES);
        scenario.attach(png, "image/png", "after-step");
    }
}

Put this class under a package included by the runner’s Cucumber glue setting. Your project’s object factory must be able to construct both TestContext and ScreenshotHooks; replace the context with your existing driver manager when needed.

2.3 A complete driver lifecycle example

The exact Cucumber and TestNG dependency versions vary by project, so keep your existing version-managed build. This example shows lifecycle placement and constructor wiring; adapt it to the object factory you already use.

package steps;

import io.cucumber.java.After;
import io.cucumber.java.Before;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

public class BrowserLifecycle {
    private static final ThreadLocal<WebDriver> CURRENT = new ThreadLocal<>();

    @Before
    public void startBrowser() {
        CURRENT.set(new ChromeDriver());
    }

    public static WebDriver driver() {
        WebDriver driver = CURRENT.get();
        if (driver == null) {
            throw new IllegalStateException("No WebDriver for this scenario thread");
        }
        return driver;
    }

    @After
    public void stopBrowser() {
        WebDriver driver = CURRENT.get();
        try {
            if (driver != null) driver.quit();
        } finally {
            CURRENT.remove();
        }
    }
}

If your dependency-injection setup creates a new hook object per scenario, pass BrowserLifecycle.driver() into the context. If it uses a shared manager, ensure that manager resolves the driver for the current scenario thread.

3. Attach a useful name and control the policy

A stable attachment name is easiest to find in HTML or JSON reports. If your formatter preserves names, include a step counter or sanitized step text. Keep the media type as image/png.

private final AtomicInteger stepNumber = new AtomicInteger();

@AfterStep
public void captureAfterStep(Scenario scenario) {
    byte[] png = ((TakesScreenshot) driver)
            .getScreenshotAs(OutputType.BYTES);
    int number = stepNumber.incrementAndGet();
    scenario.attach(png, "image/png", "after-step-" + number);
}

Use a per-scenario counter when scenarios can run in parallel; a static counter mixes names across scenarios. For failure-only policy, guard the attachment with your project’s failure state, for example scenario.isFailed(), and capture only when it is true. The default shown above intentionally captures passing and failing steps.

4. TestNG runner and glue configuration

The hook is independent of whether Cucumber scenarios are launched by JUnit or TestNG. Your TestNG runner must include the package containing feature step definitions and hooks in its glue configuration.

package runners;

import io.cucumber.testng.AbstractTestNGCucumberTests;
import io.cucumber.testng.CucumberOptions;

@CucumberOptions(
    features = "src/test/resources/features",
    glue = {"steps"},
    plugin = {"pretty", "html:target/cucumber-report.html"}
)
public class RunCucumberTest extends AbstractTestNGCucumberTests {
}

Keep the runner class and dependency versions aligned with your project. Do not add a second driver in the hook: that would capture a blank or unrelated browser.

5. Parallel scenarios and thread safety

  • Use one driver per scenario thread, commonly through ThreadLocal<WebDriver> or a scenario-scoped dependency-injection object.
  • Never store a mutable driver in a static field shared by parallel scenarios.
  • Make counters and attachment names scenario-local.
  • Quit and remove the driver in an unconditional @After block.
  • Ensure the report formatter can distinguish attachments when multiple scenarios finish concurrently.

6. Common errors and fixes

Symptom Likely cause Fix
ClassCastException on TakesScreenshot The configured driver does not implement the interface. Use a Selenium driver that supports screenshots and verify the object is the active WebDriver.
Hook never runs Hook package is outside Cucumber glue. Add its package to @CucumberOptions(glue=...).
Null or closed driver Driver lifecycle runs in another scope or before the hook. Create the driver before steps and resolve the same scenario-scoped instance in the hook.
Only some screenshots appear A prior step failed, so subsequent steps were skipped. Read the report’s first failing step; this is expected Cucumber behavior.
Attachments are not visible Formatter does not render embedded media or media type is wrong. Use a report plugin that supports attachments and pass image/png.
Parallel runs overwrite files Shared filenames or shared driver. Prefer Scenario.attach; if files are required, include scenario ID and thread ID in names.
Screenshot shows a transition or blank page Capture occurs immediately after the step action. Wait in the step for a meaningful condition, such as a visible selector or URL, before returning.
Browser quit error masks result Teardown runs before report processing or throws. Keep quit in finally and avoid throwing from cleanup after the attachment is made.

7. Performance, report size, and reliability

  • PNG bytes are embedded once per executed step, so long scenarios can create large reports. Use failure-only capture or split scenarios when full history is unnecessary.
  • Screenshot capture adds a browser round trip after every step. Keep steps focused and avoid redundant waits.
  • Capture after the step’s assertion and synchronization point; otherwise the image may represent an intermediate state.
  • For remote WebDriver, account for network latency and transient session failures. A capture error should be handled according to your reporting policy so it does not hide the original assertion failure.
  • There is no universal storage or timing figure in the Cucumber API; measure report size and suite duration in your own CI environment.

8. Or skip the browser setup

If you need rendered images outside a Cucumber run, ScreenshotNeo returns a screenshot or PDF from one GET request. Its capture pipeline accepts consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the verdict with X-Page-Verdict and X-Billed headers. It also provides an MCP server with take_screenshot, get_page_info, and capture_pdf for AI agents.

See the ScreenshotNeo API documentation for all options. The same call works from shell scripts or CI:

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Features include full-page capture with lazy images loaded, CSS-selector element capture, dark mode, device presets and custom viewports, retina scale, PDF paper and page controls, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user-agent and authorization, timezone and geolocation, transparent backgrounds, resizing, configurable caching, signed links, async webhooks, bulk capture for 100 URLs per call, and a usage API. Every feature is on every plan. 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000.

Create a free ScreenshotNeo account to get 1,000 screenshots each month with no card.

9. FAQ

Does @AfterStep run after a failed step?

Yes, for the failed step itself. Cucumber skips later steps and their hooks.

Can I attach JPEG instead of PNG?

Selenium can return other output types, but use matching bytes and media type. PNG is the straightforward lossless choice for Scenario.attach.

Do I need a TestNG-specific hook?

No. Cucumber invokes the hook; TestNG only supplies the scenario runner.

Why is my report huge?

You attached one image for every executed step. Switch to failure-only capture or reduce scenario length when full history is not required.

Can AI agents capture pages without Selenium?

Yes. ScreenshotNeo’s MCP server exposes screenshot, page-info, and PDF tools to Claude, Cursor, and other MCP clients.