Reporting and CI Integration
Turning a Maven-run Selenium suite's results into something a team can act on, running it headlessly in CI, and capturing failure screenshots automatically.
What you'll learn
- Configure Maven Surefire to produce CI-consumable test reports
- Explain what headless mode changes and why CI environments typically require it
- Capture a screenshot automatically on test failure using JUnit's TestWatcher extension
Prerequisites
Explanation
Maven Surefire (the plugin that actually runs mvn test) produces XML reports under target/surefire-reports/ by default — machine-parseable output many CI platforms consume natively to display pass/fail counts and individual test results in their own dashboards, independent of any custom reporting you add. This is the direct Maven-ecosystem equivalent of the reporter configuration covered for other automation tools: a CI-consumable, structured result format that survives past the raw console output of a single run.
Headless mode (ChromeOptions options = new ChromeOptions(); options.addArguments("--headless=new"); WebDriver driver = new ChromeDriver(options);) runs a real, fully-functional browser with no visible window rendered — CI environments typically require this, since most CI runners have no display server available at all (Xvfb, a virtual framebuffer, is one common workaround for tools that genuinely require a display; running headless avoids needing it entirely for browsers that support true headless operation). It's worth stating precisely: headless is not a simulation or a reduced-fidelity mode — it's the exact same real browser engine, actually rendering and executing the exact same page, just without displaying that rendering in a visible window. Test behavior should be identical between headed and headless runs for a correctly-written test; a test that only passes in one mode specifically often reveals a genuine bug (a race condition sensitive to rendering timing, or code that accidentally depends on window focus) rather than a limitation of headless mode itself.
Capturing a failure screenshot automatically — rather than relying on a developer to notice a failure and manually screenshot it before the browser closes — uses a JUnit 5 extension implementing TestWatcher: public class ScreenshotOnFailureExtension implements TestWatcher { public void testFailed(ExtensionContext context, Throwable cause) { WebDriver driver = ...; File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); Files.copy(screenshot.toPath(), Paths.get("failure-" + context.getDisplayName() + ".png")); } }, registered on a test class via @ExtendWith(ScreenshotOnFailureExtension.class). This is the same underlying principle as this course's earlier driver-lifecycle lesson — automate what would otherwise depend on a human remembering to do it manually, every single time, under exactly the pressure (an unexpected failure) when they're least likely to remember.
Example
Modeling headless-mode's 'same engine, no visible window' property and automatic failure-screenshot capture, as data.
function browserBehavior(headless) {
// The KEY point: headless changes only whether a window is rendered visibly --
// NOT which engine runs, or what the page actually does.
return { engine: "real-chromium", rendersPage: true, showsVisibleWindow: !headless };
}
console.log(browserBehavior(true)); // { engine: "real-chromium", rendersPage: true, showsVisibleWindow: false }
console.log(browserBehavior(false)); // { engine: "real-chromium", rendersPage: true, showsVisibleWindow: true }
// Same engine, same real rendering, in both cases -- only visibility differs.
function onTestFailure(testName, captureScreenshot) {
const path = "failure-" + testName + ".png";
captureScreenshot(path); // automated -- no human needs to remember to do this manually
return path;
}
console.log(onTestFailure("loginTest", (path) => console.log("saved to", path)));Try it yourself
Call browserBehavior(false) and confirm showsVisibleWindow is true while engine and rendersPage stay identical to the headless case.
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Guided exercise
Guided exercise
Write browserBehavior(headless) exactly as modeled: return {engine: 'real-chromium', rendersPage: true, showsVisibleWindow: !headless}.
Checks: headless mode is a real, rendering engine with no visible window · non-headless mode shows a visible window
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Independent exercise
Independent exercise
Write buildFailureScreenshotPath(testDisplayName, timestamp) returning 'failure-' + testDisplayName + '-' + timestamp + '.png', with any spaces in testDisplayName replaced with underscores (a real filename constraint). Then write shouldCaptureScreenshot(testOutcome) returning true only for 'failed' or 'aborted', false for 'passed' or 'skipped'.
Checks: builds a correctly-formatted, filesystem-safe path · correctly triggers a screenshot on failure · correctly skips screenshot capture for a passing test
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Guided local lab
Refactor Tests into Maintainable Components and Execute Them in CI
Runs on your computerRefactor your Selenium tests into page objects, add automatic failure screenshots via a JUnit extension, configure headless execution, and produce a CI-consumable report — the capstone of this course's execution and reliability work.
Required tools
- JDK (21 LTS or newer)
- Apache Maven (3.9+)
- Selenium WebDriver (4.x)
- A terminal (any)
Setup
- Continue from the selenium-learning-lab project used in this course's earlier guided local labs.
- Add a page-object package and a JUnit extension class for automatic failure screenshots.
Project structure
selenium-learning-lab/
pom.xml
src/
main/java/com/visaspark/selenium/pages/
SearchPage.java
test/java/com/visaspark/selenium/
ScreenshotOnFailureExtension.java
RefactoredWorkflowTest.javaStarter files
src/test/java/com/visaspark/selenium/ScreenshotOnFailureExtension.java
package com.visaspark.selenium;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestWatcher;
import org.openqa.selenium.*;
import java.io.File;
import java.nio.file.*;
public class ScreenshotOnFailureExtension implements TestWatcher {
@Override
public void testFailed(ExtensionContext context, Throwable cause) {
// TODO: retrieve the WebDriver instance for this test (e.g. from a static field
// or the test instance itself), call getScreenshotAs, and copy it to a real file
// named using context.getDisplayName()
}
}
src/test/java/com/visaspark/selenium/RefactoredWorkflowTest.java
package com.visaspark.selenium;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
@ExtendWith(ScreenshotOnFailureExtension.class)
class RefactoredWorkflowTest {
static WebDriver driver;
@BeforeEach
void setup() {
ChromeOptions options = new ChromeOptions();
// TODO: add "--headless=new" when running in CI (check an env var like CI)
driver = new ChromeDriver(options);
}
@AfterEach
void teardown() {
driver.quit();
}
@Test
void refactoredWorkflowUsesPageObjects() {
// TODO: use a page object (create one under src/main/java) instead of raw
// findElement calls directly in this test method
}
}
Requirements
- At least one page object class exists under src/main/java, used by the test instead of raw findElement calls.
- ScreenshotOnFailureExtension genuinely captures and saves a screenshot when a test fails.
- ChromeOptions conditionally adds '--headless=new' based on an environment variable (e.g. CI), matching the CI-vs-local branching pattern.
- `mvn test` produces a Surefire XML report under target/surefire-reports/.
Commands to run
Run the suite locally (headed, visible browser)
mvn testRun the suite as CI would (headless)
CI=true mvn testInspect the Surefire report
cat target/surefire-reports/*.xml
Expected behavior
The refactored test passes identically in both headed and headless (CI=true) modes, using a page object rather than raw locator calls directly in the test. Deliberately breaking the test (temporarily) produces a real screenshot file via the extension. A Surefire XML report exists after every run.
Verify it yourself
mvn testExpected: BUILD SUCCESS; a real, visible Chrome window is observed
CI=true mvn testExpected: BUILD SUCCESS; no visible browser window appears, but the test still passes identically
ls target/surefire-reports/Expected: At least one .xml report file exists
(temporarily break the test's assertion) mvn testExpected: The test fails, AND a real failure-*.png screenshot file is created by the extension
Troubleshooting
- Headless mode fails but headed mode passes — This is a real signal worth investigating, not dismissing — check for code that accidentally depends on window focus or a rendering-timing-sensitive race condition, rather than assuming headless mode itself is broken.
- ScreenshotOnFailureExtension doesn't produce a file — Confirm the extension can actually access the same WebDriver instance the failing test used — a common approach is a static field or a JUnit store, since the extension and the test method aren't otherwise directly connected.
- No surefire-reports directory after running — Confirm you're running via `mvn test` (which invokes Surefire) rather than running the test class directly through an IDE, which may not produce the same Maven-managed report output.
Stuck? Get a hint.
Extension challenge
Configure a GitHub Actions (or equivalent) workflow file that runs `CI=true mvn test` on every push, uploading target/surefire-reports/ and any failure-*.png files as CI artifacts, so a failure's screenshot is genuinely retrievable from the CI run itself, not just from a local machine.
When you've verified this locally, use the "Mark lesson complete" button below to record your progress.
Common mistakes
- Leaving raw findElement calls scattered directly in test methods instead of extracting a page object, especially once the same locators are needed across more than one test -- this is exactly the reuse-earns-the-structure principle from earlier in this course, applied at the point it starts to matter.
- Assuming a test failing only in headless mode means headless mode itself is broken, rather than investigating a real, hidden bug (a focus-dependent interaction, a rendering-timing race) the headed run happened to mask.
- Relying on a human to notice a failure and manually screenshot it before the browser window closes -- this depends on someone watching at exactly the right moment; an automated TestWatcher-based capture removes that dependency entirely.
Knowledge check
Takeaway
Maven Surefire produces CI-consumable XML reports automatically; headless mode is the same real browser engine with no visible window, so behavior differences between headed and headless runs are a real signal worth investigating, not a headless-mode limitation; and automating failure-screenshot capture via TestWatcher removes the dependency on a human noticing and acting at the right moment.
Summary
Maven Surefire's XML reports under target/surefire-reports/ are natively consumable by most CI platforms. Headless mode runs the identical real browser engine with no visible window — a test that only passes in one mode usually reveals a real bug. A JUnit TestWatcher extension automates failure-screenshot capture, removing reliance on manual, human-timed intervention.
References
Your notes
Notes save automatically.
Finished this lesson?
Mark it complete to track your progress and schedule a future review.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.