Skip to main content
SeleniumDecoded

Reporting: Allure, ExtentReports and JUnit XML

Turn raw test output into reports people read: JUnit XML for CI, Allure for rich history and attachments, ExtentReports for single-file HTML, with screenshots, logs and steps attached automatically.

Selenium 4 Stable Updated 9 Sept 2026 · Verified against Selenium 4.48.0

A failing Selenium test is only useful if someone can see what happened without rerunning it. Good reporting attaches a screenshot, the page source, the console errors and the steps leading up to the failure, and keeps history so flaky tests stand out. This lesson covers the three formats you will meet and how to wire Selenium evidence into each.

Three Formats, Three Jobs

FormatBest forProduced by
JUnit XMLCI systems (GitHub, GitLab, Jenkins) that render pass/fail and trends nativelyEvery test runner, out of the box
AllureRich, interactive reports with steps, attachments, history, retries and categoriesallure-* adapters plus the Allure CLI or server
ExtentReportsA single self-contained HTML file you can email or archiveThe ExtentReports library (Java, .NET; community ports elsewhere)

Always emit JUnit XML; it costs nothing and CI dashboards depend on it. Add Allure when the team needs to browse failures with evidence, and Extent when you need one portable file.

JUnit XML

Enable XML output
Selenium 4 Stable
<!-- Maven Surefire writes target/surefire-reports/*.xml by default -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
<configuration>
<reportsDirectory>target/surefire-reports</reportsDirectory>
</configuration>
</plugin>
# pytest
pytest tests/e2e --junitxml=reports/junit.xml
# Attach extra data to the XML from a fixture:
def test_checkout(driver, record_property):
record_property("browser", driver.capabilities["browserName"])
// mocha with mocha-junit-reporter
// npm i -D mocha-junit-reporter
mocha --reporter mocha-junit-reporter --reporter-options mochaFile=reports/junit.xml
// Or multiple reporters at once: npm i -D mocha-multi-reporters
// dotnet test with the JUnit logger (NuGet: JunitXml.TestLogger)
dotnet test --logger "junit;LogFilePath=reports/junit.xml"
// TRX is the built-in alternative most .NET CI plugins understand
dotnet test --logger trx --results-directory reports

Allure

Allure’s adapters hook into the test lifecycle and write result JSON; allure generate turns a folder of results into a site with history when you keep the previous run’s history/ directory. Attachments and steps are the parts that matter for Selenium.

Steps and automatic screenshot on failure
Selenium 4 Stable
// build.gradle: testImplementation "io.qameta.allure:allure-junit5:2.29.0"
import io.qameta.allure.Allure;
import io.qameta.allure.Attachment;
import io.qameta.allure.Step;
public class CheckoutPage extends BasePage {
@Step("Add {sku} to the basket")
public CheckoutPage addToBasket(String sku) {
wait.until(ExpectedConditions.elementToBeClickable(testId("add-" + sku))).click();
return this;
}
}
// JUnit 5 extension that attaches evidence when a test fails
public class AllureEvidenceExtension implements TestWatcher {
@Override
public void testFailed(ExtensionContext ctx, Throwable cause) {
WebDriver driver = DriverManager.get();
Allure.addAttachment("Screenshot", "image/png",
new ByteArrayInputStream(((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES)), "png");
Allure.addAttachment("Page source", "text/html", driver.getPageSource(), "html");
Allure.addAttachment("URL", driver.getCurrentUrl());
}
}
// Generate: ./gradlew test; allure serve build/allure-results
# pip install allure-pytest
# pytest --alluredir=allure-results ; allure serve allure-results
import allure
import pytest
class CheckoutPage(BasePage):
@allure.step("Add {sku} to the basket")
def add_to_basket(self, sku):
self.wait.until(EC.element_to_be_clickable(test_id(f"add-{sku}"))).click()
return self
# conftest.py: attach evidence on failure
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
driver = item.funcargs.get("driver")
if driver:
allure.attach(driver.get_screenshot_as_png(), "Screenshot", allure.attachment_type.PNG)
allure.attach(driver.page_source, "Page source", allure.attachment_type.HTML)
allure.attach(driver.current_url, "URL", allure.attachment_type.TEXT)
// npm i -D allure-mocha allure-js-commons
// mocha --reporter allure-mocha ; allure serve allure-results
const { step, attachment } = require('allure-js-commons');
class CheckoutPage extends BasePage {
async addToBasket(sku) {
await step(`Add ${sku} to the basket`, async () => {
const btn = await this.driver.wait(until.elementLocated(testId(`add-${sku}`)), 10000);
await btn.click();
});
return this;
}
}
afterEach(async function () {
if (this.currentTest.state === 'failed') {
await attachment('Screenshot', Buffer.from(await driver.takeScreenshot(), 'base64'), 'image/png');
await attachment('Page source', await driver.getPageSource(), 'text/html');
await attachment('URL', await driver.getCurrentUrl(), 'text/plain');
}
});
Allure.NUnit
using Allure.NUnit;
using Allure.NUnit.Attributes;
using Allure.Net.Commons;
[AllureNUnit]
public class CheckoutTests
{
[TearDown]
public void AttachEvidence()
{
if (TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Failed)
{
AllureApi.AddAttachment("Screenshot", "image/png", ((ITakesScreenshot)_driver).GetScreenshot().AsByteArray);
AllureApi.AddAttachment("Page source", "text/html", Encoding.UTF8.GetBytes(_driver.PageSource));
AllureApi.AddAttachment("URL", "text/plain", Encoding.UTF8.GetBytes(_driver.Url));
}
_driver.Quit();
}
[Test]
[AllureStep("Add {sku} to the basket")]
public void AddToBasket(string sku) { /* ... */ }
}

Useful Allure extras:

  • Categories (categories.json) classify failures by exception message so “timeout” and “assertion” are counted separately.
  • History appears when you copy allure-report/history into the next run’s allure-results before generating. CI jobs usually restore it from the previous artifact.
  • Environment (environment.properties) records browser, Grid URL and build so a report says what it tested.
  • Retries are shown as such, with each attempt’s attachments, which makes flaky tests visible instead of hidden.

ExtentReports

Extent produces a single HTML file with a dashboard, per-test logs and embedded screenshots. It is popular in Java and .NET shops where reports are archived or emailed.

Single-file HTML report
Selenium 4 Stable
// build.gradle: testImplementation "com.aventstack:extentreports:5.1.2"
import com.aventstack.extentreports.*;
import com.aventstack.extentreports.reporter.ExtentSparkReporter;
public final class Report {
private static final ExtentReports EXTENT = new ExtentReports();
private static final ThreadLocal<ExtentTest> TEST = new ThreadLocal<>();
static {
ExtentSparkReporter spark = new ExtentSparkReporter("target/extent/report.html");
spark.config().setDocumentTitle("E2E results");
EXTENT.attachReporter(spark);
EXTENT.setSystemInfo("Browser", TestConfig.browser());
Runtime.getRuntime().addShutdownHook(new Thread(EXTENT::flush));
}
public static void start(String name) { TEST.set(EXTENT.createTest(name)); }
public static ExtentTest current() { return TEST.get(); }
public static void finish() { TEST.remove(); }
}
// In the failure hook
String base64 = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BASE64);
Report.current().fail(cause, MediaEntityBuilder.createScreenCaptureFromBase64String(base64).build());
# No official Extent for Python. pytest-html gives a comparable single-file report:
# pip install pytest-html ; pytest --html=reports/report.html --self-contained-html
import pytest
from pytest_html import extras
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
driver = item.funcargs.get("driver")
if driver:
report.extras = [
extras.png(driver.get_screenshot_as_png()),
extras.url(driver.current_url),
]
// mochawesome is the JavaScript equivalent: a single HTML report with attachments
// npm i -D mochawesome ; mocha --reporter mochawesome
const addContext = require('mochawesome/addContext');
afterEach(async function () {
if (this.currentTest.state === 'failed') {
const png = await driver.takeScreenshot();
addContext(this, { title: 'Screenshot', value: `data:image/png;base64,${png}` });
addContext(this, { title: 'URL', value: await driver.getCurrentUrl() });
}
});
// NuGet: ExtentReports
using AventStack.ExtentReports;
using AventStack.ExtentReports.Reporter;
public static class Report
{
private static readonly ExtentReports Extent = new();
private static readonly ThreadLocal<ExtentTest> Current = new();
static Report()
{
var spark = new ExtentSparkReporter("reports/extent/report.html");
Extent.AttachReporter(spark);
AppDomain.CurrentDomain.ProcessExit += (_, _) => Extent.Flush();
}
public static void Start(string name) => Current.Value = Extent.CreateTest(name);
public static ExtentTest Test => Current.Value!;
}
// On failure:
var base64 = ((ITakesScreenshot)_driver).GetScreenshot().AsBase64EncodedString;
Report.Test.Fail(ex.Message, MediaEntityBuilder.CreateScreenCaptureFromBase64String(base64).Build());

What to Attach, and What Not To

Attach, on failure only:

  • Screenshot (PNG, of the viewport or the element under test)
  • Page source (HTML)
  • Current URL and window title
  • Console errors captured through BiDi during the test
  • Failed network requests (status 4xx/5xx) captured through BiDi
  • Grid session id and, if recording, a link to the video

Do not attach on every test: screenshots per step balloon report size and slow the suite. Do not attach secrets: page source can contain tokens; scrub or skip it on login pages.

Steps as Documentation

The @Step / allure.step annotations on page object methods turn a report into a readable narrative: “Open checkout, Add SKU-42 to the basket, Apply coupon SAVE10, Place order, then failed at: Verify confirmation number.” Name steps from the user’s perspective and interpolate the arguments; it costs nothing and makes reports useful to product owners.

Summary

  • Emit JUnit XML always; add Allure for interactive reports with history, or Extent for single-file HTML.
  • Attach screenshot, page source, URL, console and network errors on failure via your framework’s after-hook.
  • Annotate page object methods as steps so reports read as user journeys.
  • Keep history between runs so flaky tests become visible.

Copy-paste recipes for this topic

Related lessons