Screenshot on Failure in Cucumber JVM
What Is Screenshot on Failure?
Screenshot on failure is the practice of automatically capturing the browser screen when a Cucumber scenario fails. In a Selenium and Cucumber JVM framework, this usually means taking a screenshot at the end of a failed scenario and attaching it to the Cucumber report. The screenshot becomes visual evidence of the application state at the moment the test failed.
This concept is simple, but it is extremely important in real automation projects. A failure message may tell you that an element was not found, an assertion failed, or a timeout occurred. However, that message does not always show what the user actually saw. The page may not have loaded fully, a modal may have blocked the button, a validation message may have appeared, or the test may have reached a different page than expected. A screenshot helps connect the technical failure with the visible application state.
In practical terms, screenshot on failure means this: if a scenario fails, capture the browser screen before closing the browser. That one rule saves a large amount of debugging time. Instead of rerunning the test immediately and guessing what happened, the tester can open the report, look at the screenshot, and understand the failure context more quickly.
For Cucumber JVM, screenshots are usually captured inside an @After hook because hooks run after every scenario. The hook receives a Scenario object, checks whether the scenario failed, captures the screenshot through Selenium WebDriver, attaches the screenshot to the report, and then performs teardown such as quitting the browser.
Why Screenshot on Failure Is Important
Failed automation without screenshots often gives incomplete information. A message such as NoSuchElementException only says that Selenium could not find an element. It does not explain whether the user was on the wrong page, whether the element was hidden behind another layer, whether a spinner was still loading, whether login failed, or whether a popup interrupted the flow. A screenshot gives the missing visual context.
In manual testing, a tester naturally observes the screen while executing a test. In automation, the test runs without a human watching every step. When a failure occurs, the report must become the observer. Logs, stack traces, screenshots, browser console messages, and request details together help explain the failure. Among these, screenshots are often the fastest artifact to interpret because the application state is visible immediately.
Screenshots are especially useful for UI automation because many UI failures are state-related. The same locator may work on one run and fail on another because the page is slow, the element is disabled, the layout shifted, the browser size changed, or a user-specific banner appeared. A screenshot can show these differences clearly. It helps distinguish an application bug from a test synchronization problem.
They also improve collaboration. Developers may not know the exact automation flow, and testers may not have access to server logs. A screenshot gives both groups a shared reference. Instead of saying "the checkout test failed at payment," the report can show that the payment button was disabled, the address section showed an error, or the user was redirected to the login page.
Best Place to Capture Screenshot
The best place to capture a failure screenshot in Cucumber JVM is usually the @After hook. This hook runs after each scenario, regardless of whether the scenario passed or failed. Because it runs after the scenario, it has access to the final state of execution. It is also the right place because cleanup and teardown are usually handled there.
The sequence should be carefully ordered. First the scenario executes. If it fails, the hook captures the screenshot while the browser is still open. After the screenshot is attached to the report, the framework can close the browser, clean test data, close connections, and flush reports. If the browser is closed before the screenshot is captured, the framework loses the most useful visual evidence.
A common mistake is placing screenshot logic inside catch blocks scattered across step definitions. That approach creates duplication and still misses failures that happen outside those catch blocks. A centralized @After hook is cleaner because it applies consistently to every scenario. It also keeps step definitions focused on behavior instead of reporting mechanics.
There are some advanced cases where screenshots may also be captured during intermediate steps, such as after critical business milestones or before risky interactions. However, the default failure screenshot should remain in the @After hook because it provides one reliable place for failure evidence across the whole framework.
Using the Scenario Object
Cucumber JVM provides a Scenario object that can be injected into hooks. This object represents the current scenario being executed. It exposes useful information such as the scenario name, status, tags, and whether the scenario failed. The method most commonly used for screenshot logic is scenario.isFailed().
When scenario.isFailed() returns true, the framework knows that the scenario did not complete successfully. At that point, the hook can capture a screenshot and attach it to the report. When it returns false, the hook can skip screenshot capture and continue with normal teardown. This avoids filling reports with unnecessary screenshots for passed scenarios.
@After
public void tearDown(Scenario scenario) {
if (scenario.isFailed()) {
// capture screenshot
}
}
This pattern is simple and readable. It expresses the intent clearly: capture evidence only when the scenario fails. It also separates reporting logic from test steps. The step definitions do not need to know how screenshots are handled. They only execute the behavior. The hook handles failure evidence consistently after execution.
Basic Screenshot Code
In Selenium WebDriver, screenshots are captured through the TakesScreenshot interface. Most browser driver implementations support this interface. The driver is cast to TakesScreenshot, and the screenshot is requested using getScreenshotAs(). For Cucumber report attachments, OutputType.BYTES is normally the cleanest option.
@After
public void tearDown(Scenario scenario) {
if (scenario.isFailed()) {
TakesScreenshot ts = (TakesScreenshot) driver;
byte[] screenshot = ts.getScreenshotAs(OutputType.BYTES);
scenario.attach(
screenshot,
"image/png",
"Failure Screenshot"
);
}
driver.quit();
}
This code checks the scenario status, captures the screenshot as bytes, attaches it to the scenario report, and then quits the driver. The order is important. The driver must still be alive when the screenshot is captured. If driver.quit() runs first, Selenium cannot capture the browser state.
The exact driver variable may come from a driver factory, dependency injection container, base test class, or scenario context. The structure may differ from project to project, but the idea remains the same. The screenshot logic should use the active driver for the current scenario and should not accidentally use a driver from another scenario or thread.
Required Imports
The basic implementation needs Cucumber hook and scenario imports, along with Selenium screenshot imports. These imports make the intent of the hook clear and avoid framework confusion.
import io.cucumber.java.After;
import io.cucumber.java.Scenario;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
If the framework uses a driver manager or factory, additional imports may be required for that class. If screenshots are saved as files, imports such as java.io.File, java.nio.file.Files, or Apache Commons IO may also be used. However, for direct report attachment in Cucumber, byte-based attachment is usually enough.
Why Use OutputType.BYTES?
OutputType.BYTES returns the screenshot as a byte array. This is convenient because Cucumber's scenario.attach() method can directly attach binary data to the report. There is no need to create a physical file first, read it back, and then attach it. The report receives the image content directly.
Using bytes keeps the implementation compact and reduces file-handling errors. File-based screenshots can be useful when the team wants a separate screenshot folder, external evidence archive, or custom reporting integration. But if the goal is to embed the screenshot in the Cucumber report, byte output is the most direct approach.
Another advantage is portability. Byte attachments work well in local execution and CI execution because they do not depend heavily on a fixed local folder path. File paths can differ between Windows machines, Linux build agents, Docker containers, and cloud runners. Attaching bytes avoids many path-related problems.
Attaching Screenshots to Cucumber Reports
Attaching the screenshot to the Cucumber report is what makes the screenshot useful to the team. Capturing a screenshot but storing it in an unknown folder is less effective because users may not know where to find it. When the screenshot appears directly inside the scenario report, failure analysis becomes much faster.
The scenario.attach() method accepts the screenshot data, media type, and attachment name. For PNG screenshots, the media type should be image/png. The attachment name can be something simple like Failure Screenshot, or it can include the scenario name if the reporting style benefits from that.
scenario.attach(screenshot, "image/png", "Failure Screenshot");
Most teams prefer attaching only failed-scenario screenshots because it keeps reports smaller and easier to review. Attaching screenshots for every step or every passed scenario can make reports heavy, slow to open, and difficult to navigate. Failure screenshots give strong value without overwhelming the report.
Screenshot Before Browser Teardown
The most important rule is to capture the screenshot before browser teardown. The screenshot depends on the browser session being alive. If the framework closes the driver first, the screenshot code may throw a NoSuchSessionException, WebDriverException, or return no useful output.
A strong teardown sequence captures diagnostics first and releases resources second. Diagnostics may include screenshots, page source, browser console logs, network logs, current URL, page title, and custom application logs. After these artifacts are collected, the framework can safely quit the driver and close other resources.
@After
public void afterScenario(Scenario scenario) {
if (scenario.isFailed()) {
captureScreenshot(scenario);
attachCurrentUrl(scenario);
attachPageTitle(scenario);
}
driver.quit();
}
This ordering is useful because screenshots alone may not always explain the failure. Pairing the screenshot with the current URL and page title gives additional context. For example, if the screenshot shows a login page when the scenario expected checkout, the current URL confirms the redirect.
Using a Driver Factory
Real Cucumber JVM frameworks rarely keep WebDriver as a public variable directly inside hook classes. Most use a driver factory or driver manager. This centralizes browser creation and teardown and makes the framework easier to maintain. Screenshot logic should use the same source of truth for the current driver.
@After
public void tearDown(Scenario scenario) {
WebDriver driver = DriverFactory.getDriver();
if (scenario.isFailed() && driver != null) {
byte[] screenshot =
((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
scenario.attach(screenshot, "image/png", "Failure Screenshot");
}
DriverFactory.quitDriver();
}
This approach avoids duplicating driver ownership in multiple classes. The hook asks the driver factory for the active driver, captures the screenshot if needed, and then delegates teardown back to the factory. The hook remains readable and the driver lifecycle remains centralized.
When frameworks support parallel execution, the driver factory usually uses ThreadLocal<WebDriver>. This ensures that each scenario receives its own browser instance. Screenshot logic must use the current thread's driver, not a static shared driver, otherwise a screenshot from one scenario may be attached to another scenario's report.
Parallel Execution Considerations
Parallel execution makes screenshot handling more important and more delicate. When multiple scenarios run at the same time, each scenario may fail independently. Each failure needs the screenshot from its own browser session. If the driver is stored incorrectly, screenshots can become mixed, overwritten, or attached to the wrong scenario.
The safest design is to make WebDriver scenario-scoped or thread-scoped. Each scenario should have its own driver instance, and the hook should access only that instance. If screenshots are saved to disk, filenames must also be unique. Names based only on the scenario name may collide when the same scenario runs across multiple browsers or environments.
A good filename may include the scenario name, browser name, environment, timestamp, and thread id. For example, failed_login_chrome_qa_20260816_103015_12.png is much safer than screenshot.png. The same principle applies to report attachment names if the report displays multiple screenshots.
Parallel-safe screenshot handling also means avoiding shared mutable objects inside hooks. A shared screenshot folder is acceptable if filenames are unique. A shared WebDriver instance is not. A shared report object may be acceptable if the reporting library is designed for parallel execution, but it must be configured correctly.
File-Based Screenshots
Although Cucumber attachments work well with bytes, some teams also save screenshots as files. File-based screenshots are useful when the organization wants to archive evidence outside the Cucumber report, upload artifacts to CI, link screenshots from defect tickets, or keep screenshots for a longer retention period.
File source = ((TakesScreenshot) driver)
.getScreenshotAs(OutputType.FILE);
Files.copy(
source.toPath(),
Paths.get("screenshots", fileName)
);
When saving files, the framework should create the screenshot directory if it does not exist, sanitize scenario names, and avoid characters that are invalid in file paths. Scenario names often contain spaces, punctuation, or special characters. A small utility method can convert the scenario name into a safe filename.
File-based screenshots should not replace report attachments unless the team has a clear reason. The best user experience is often to do both: attach the screenshot to the report for quick review and save it as a file for CI artifacts or long-term debugging. However, this should be balanced against storage size and retention policies.
Screenshot Naming Strategy
A clear naming strategy makes screenshots easier to use. Random names are hard to trace. Overly simple names are easy to overwrite. A useful screenshot name should describe what failed and when it happened. It should also be safe for the operating system and unique enough for repeated execution.
A practical naming pattern includes a cleaned scenario name and timestamp. In CI, adding build number, browser name, or environment can make the screenshot even more useful. The filename should avoid slashes, colons, question marks, and other characters that may break file handling on Windows or Linux.
String safeName = scenario.getName()
.replaceAll("[^a-zA-Z0-9-_]", "_");
String fileName = safeName + "_" +
System.currentTimeMillis() + ".png";
The naming strategy should be implemented once in a utility method. Every hook, listener, or report integration should reuse it. This prevents inconsistent file names across the framework and makes screenshot storage predictable.
Handling Screenshot Failures
Screenshot capture itself can fail. The browser may already be closed, the driver session may be invalid, the page may be in an unstable state, or the remote WebDriver connection may be lost. A screenshot failure should be handled carefully because it happens during failure processing. The framework should not hide the original scenario failure behind a secondary screenshot exception.
A good implementation wraps screenshot capture in a small try-catch block. If screenshot capture fails, the hook can log the problem and continue teardown. The original scenario result should remain visible. This is especially important in CI reports, where a hook exception may make the root cause harder to understand.
try {
byte[] screenshot =
((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
scenario.attach(screenshot, "image/png", "Failure Screenshot");
} catch (Exception e) {
scenario.log("Screenshot capture failed: " + e.getMessage());
}
This does not mean screenshot failures should be ignored forever. If screenshots frequently fail, the framework has a lifecycle problem. The team should investigate whether the driver is being closed too early, whether hooks are ordered incorrectly, or whether parallel execution is using the wrong driver instance.
Screenshot and Hook Execution Order
When multiple @After hooks exist, hook order becomes important. One hook may capture screenshots, another may clean data, another may quit the browser, and another may flush reports. If the browser teardown hook runs before the screenshot hook, screenshots will fail. If the report is flushed before screenshots are attached, the screenshot may not appear in the final report.
Cucumber supports ordered hooks. Teams can use hook order to make the lifecycle explicit. Diagnostics should run before resource teardown. Resource teardown should run before final report shutdown. Data cleanup may need to run before API clients or database connections are closed. The exact sequence depends on the framework, but it should be intentional.
Even when the framework uses only one @After hook, the internal order inside that hook matters. Capture screenshot first, attach useful logs, clean scenario-created data if needed, close technical resources, and then flush reports. Keeping this order consistent prevents confusing failures during teardown.
Screenshot with Page Source and Logs
A screenshot is powerful, but it does not capture everything. It shows the visible page, but it may not show hidden DOM elements, JavaScript errors, network failures, or backend messages. For difficult failures, combining screenshots with page source and logs provides better evidence.
For example, if an element is not visible, the screenshot may show a blank area. The page source may confirm whether the element was present but hidden, absent from the DOM, or rendered with a different attribute. Browser console logs may show a JavaScript error that prevented the element from rendering. Current URL and page title may reveal a redirect or navigation issue.
Cucumber's Scenario object can attach text as well as images. Teams can attach the current URL, page title, or short diagnostic messages. They should avoid attaching huge page sources for every failure unless there is a clear need, because reports can become large. A balanced strategy captures the most useful evidence without making reports difficult to open.
Remote WebDriver and Grid Considerations
Screenshot handling works with local browsers and remote browsers, but remote execution adds a few details. When tests run on Selenium Grid, cloud browsers, or containers, the browser does not run on the same machine as the test code. Selenium still returns the screenshot through the WebDriver protocol, so byte attachments usually work well.
File-based screenshot paths require more care in remote execution. A file saved inside a Docker container or build agent may not be visible on the developer's machine. CI systems need artifact collection configured correctly. If screenshots are attached directly to Cucumber reports, they travel with the report and are easier to review.
Cloud testing platforms may also provide their own screenshots, videos, network logs, and session links. A mature framework can attach the cloud session URL to the Cucumber report along with the local screenshot. This helps testers jump from the Cucumber failure to the full remote execution recording when deeper analysis is needed.
Common Mistakes
Capturing After Quit
The most common mistake is calling driver.quit() before taking the screenshot. Once the WebDriver session is closed, Selenium cannot capture the browser state. The screenshot hook must run before browser teardown.
Taking Screenshots for Every Scenario
Capturing screenshots for every passed scenario can create large reports without much value. In most frameworks, screenshots should be captured on failure by default. Extra screenshots can be added for selected critical flows when there is a strong reason.
Using a Shared Static Driver
A shared static driver can cause serious problems in parallel execution. Screenshots may be captured from the wrong browser or attached to the wrong report. Driver ownership should be scenario-scoped or thread-scoped.
Overwriting Screenshot Files
Saving every screenshot as screenshot.png leads to overwritten evidence. File names should be unique and meaningful, especially in CI and parallel execution.
Hiding Original Failures
If screenshot capture throws an exception, the framework should not hide the original scenario failure. Screenshot logic should handle its own errors and continue teardown.
Best Practices
Capture screenshots in an @After hook only when the scenario fails. Capture the screenshot before quitting the browser. Attach the screenshot directly to the Cucumber report using scenario.attach() and OutputType.BYTES. Keep screenshot logic centralized in hooks or utility classes rather than duplicating it across step definitions.
Use a driver factory or scenario-scoped driver manager so screenshot logic always uses the correct browser session. Make screenshot handling parallel-safe. If saving screenshots as files, use unique sanitized names. Add timestamps, browser names, or build identifiers when needed. Avoid massive screenshot collections unless the team has a defined retention policy.
Handle screenshot failures gracefully and log them clearly. Combine screenshots with useful context such as current URL, page title, and selected logs. Keep hook order intentional so screenshots are captured before browser teardown and before final report flushing. Review screenshot strategy whenever the framework adds parallel execution, cloud execution, new reports, or Docker-based test runs.
Real-Time Framework Example
In a real framework, screenshot handling usually sits inside a hooks class, while WebDriver access is handled by a driver factory. The hook should not know how the browser was created. It only asks the factory for the current driver and captures evidence if the scenario failed.
public class Hooks {
@After
public void afterScenario(Scenario scenario) {
WebDriver driver = DriverFactory.getDriver();
if (scenario.isFailed() && driver != null) {
ScreenshotUtil.attachScreenshot(driver, scenario);
ScenarioLogUtil.attachUrlAndTitle(driver, scenario);
}
DriverFactory.quitDriver();
}
}
The screenshot utility can contain the Selenium-specific code. This keeps the hook readable and makes the utility reusable if another listener or reporting class needs the same behavior.
public class ScreenshotUtil {
public static void attachScreenshot(
WebDriver driver,
Scenario scenario) {
try {
byte[] screenshot =
((TakesScreenshot) driver)
.getScreenshotAs(OutputType.BYTES);
scenario.attach(
screenshot,
"image/png",
"Failure Screenshot"
);
} catch (Exception e) {
scenario.log("Unable to capture screenshot: "
+ e.getMessage());
}
}
}
This structure scales better than placing screenshot code directly inside every step. It also makes future changes easier. If the team later wants to save files, add cloud session links, attach console logs, or change report naming, the change can be made in one place.
Using Screenshots in CI Failure Analysis
Screenshot on failure becomes even more valuable when tests run in CI pipelines such as Jenkins, GitHub Actions, Azure DevOps, or GitLab CI. In local execution, a tester may rerun the test and observe the browser directly. In CI, the browser often runs on a remote machine, a Linux agent, a Docker container, or a Selenium Grid node. The person reviewing the failure may not have seen the execution at all. The screenshot becomes the first visual clue.
For CI usage, the report should be easy to download and inspect. If screenshots are attached to the Cucumber report, the build artifacts should include the full report folder. If screenshots are saved separately, the screenshots folder should also be published as an artifact. A common mistake is generating screenshots successfully but not preserving them after the pipeline finishes. In that case, the framework did the work, but the team still cannot use the evidence.
CI failure analysis also benefits from consistent naming and environment information. A screenshot from a failed scenario should ideally be traceable to the build number, branch, browser, environment, and scenario. This does not mean every detail must appear in the attachment title, but the report should make the context clear. When the same scenario runs across Chrome, Edge, and Firefox, a screenshot without browser information may create confusion.
Teams should avoid treating screenshots as a replacement for proper logs. A screenshot can show that the page is blank, but logs may explain whether the API returned an error, JavaScript failed, or authentication expired. The strongest CI reports combine screenshots, current URL, page title, failure stack trace, and selected logs. This gives testers a complete starting point before they rerun anything.
Good screenshot strategy also reduces unnecessary reruns. Without visual evidence, teams often rerun failures just to understand what happened. With screenshots attached, many failures can be triaged immediately. A tester may identify a locator problem, a data issue, a real UI bug, or a synchronization problem from the report itself. This makes automation feedback faster and makes CI results more trustworthy.
Interview-Ready Explanation
Screenshot on failure means automatically capturing and attaching a browser screenshot when a Cucumber scenario fails. In Cucumber JVM, this is usually implemented in an @After hook by using the Scenario object to check scenario.isFailed(). If the scenario failed, Selenium's TakesScreenshot interface captures the screen, and scenario.attach() embeds the screenshot in the report.
The screenshot must be captured before calling driver.quit(), because the browser session must still be active. A good implementation is centralized, parallel-safe, handles screenshot errors gracefully, and attaches meaningful evidence to the report. This makes UI automation failures easier to debug and helps teams distinguish application issues from automation issues.
Summary
Screenshot on failure is one of the most useful debugging practices in Cucumber JVM Selenium frameworks. It provides visual evidence at the moment a scenario fails and makes reports more actionable. Without screenshots, testers often depend only on stack traces, which may not explain the visible application state.
The key rules are straightforward: capture screenshots in an @After hook, check scenario.isFailed(), use TakesScreenshot, attach bytes to the Cucumber report, capture before browser teardown, and keep the logic centralized. When implemented well, screenshot on failure turns a failed automation run from a vague technical error into a clear, reviewable piece of evidence.