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.
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
| Format | Best for | Produced by |
|---|---|---|
| JUnit XML | CI systems (GitHub, GitLab, Jenkins) that render pass/fail and trends natively | Every test runner, out of the box |
| Allure | Rich, interactive reports with steps, attachments, history, retries and categories | allure-* adapters plus the Allure CLI or server |
| ExtentReports | A single self-contained HTML file you can email or archive | The 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
<!-- 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># pytestpytest 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-reportermocha --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 understanddotnet test --logger trx --results-directory reportsAllure
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.
// 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 failspublic 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-resultsimport allureimport 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-resultsconst { 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');}});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/historyinto the next run’sallure-resultsbefore 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.
// 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 hookString 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-htmlimport pytestfrom 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 mochawesomeconst 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: ExtentReportsusing 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
- Take a Screenshot on Test Failure Automatically
A framework hook for JUnit 5, pytest, Mocha and NUnit that saves a screenshot and page source only when a test fails.