08 · Reporting (Allure, ExtentReports)¶
A test run produces two audiences. The suite tells you what broke. A report tells the release manager, the developer and the product owner what state the build is in — with screenshots, timings, trends and a defect trail. In Level 1 you wrote an execution summary by hand; this module generates it, and makes every failure carry the evidence needed to raise a bug report.
1. What you already have¶
Surefire and Failsafe write machine-readable XML into
target/surefire-reports/ and target/failsafe-reports/ on every run. That
JUnit XML format is the lingua franca: Jenkins, GitHub Actions, GitLab and
every reporting tool below consume it.
mvn clean verify
ls target/failsafe-reports/
# TEST-com.example.tests.LoginIT.xml LoginIT.txt failsafe-summary.xml
# A readable HTML view of exactly that XML, no extra dependencies
mvn surefire-report:report
open target/site/surefire-report.html
Start here. If your CI already ingests the XML, you may not need anything else; add Allure or Extent when you actually need screenshots and steps.
2. Allure — the reporting standard¶
<dependency>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-testng</artifactId> <!-- or allure-junit5 -->
<version>2.27.0</version>
<scope>test</scope>
</dependency>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<argLine>
-javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/1.9.22/aspectjweaver-1.9.22.jar"
</argLine>
<systemPropertyVariables>
<allure.results.directory>${project.build.directory}/allure-results</allure.results.directory>
</systemPropertyVariables>
</configuration>
</plugin>
Allure works in two stages: the run writes raw JSON into
target/allure-results/, and a separate command turns that into HTML.
mvn clean verify
allure serve target/allure-results # opens a local report
allure generate target/allure-results -o target/allure-report --clean
Annotating tests¶
package com.example.tests;
import io.qameta.allure.*;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
@Epic("Authentication")
@Feature("Login")
public class LoginAllureIT {
@Test(description = "Valid credentials reach the secure area")
@Story("A registered user can sign in")
@Severity(SeverityLevel.BLOCKER)
@Link(name = "REQ-02", url = "https://wiki.example.com/reqs/REQ-02")
@Issue("BUG-142")
public void validLogin() {
openLoginPage();
submitCredentials("tomsmith", "SuperSecretPassword!");
assertEquals(currentHeading(), "Secure Area");
}
@Step("Open the login page")
private void openLoginPage() { /* ... */ }
@Step("Submit credentials for user {0}")
private void submitCredentials(String user, String password) { /* ... */ }
private String currentHeading() { return "Secure Area"; }
}
@Step is what makes an Allure report worth generating: each annotated
method becomes a collapsible line in the report, with its parameters, so a
failure shows exactly which step broke rather than a bare stack trace.
Attaching screenshots and logs¶
import io.qameta.allure.Allure;
import io.qameta.allure.Attachment;
import org.openqa.selenium.*;
import java.io.ByteArrayInputStream;
public final class AllureAttachments {
@Attachment(value = "Screenshot", type = "image/png")
public static byte[] screenshot(WebDriver driver) {
return ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
}
@Attachment(value = "Page source", type = "text/html")
public static String pageSource(WebDriver driver) {
return driver.getPageSource();
}
/** The non-annotation form, usable anywhere. */
public static void attachPng(String name, byte[] png) {
Allure.addAttachment(name, "image/png", new ByteArrayInputStream(png), ".png");
}
}
Call them from the ITestListener you built in Module 03:
@Override
public void onTestFailure(ITestResult result) {
WebDriver driver = DriverFactory.get();
if (driver != null) {
AllureAttachments.screenshot(driver);
AllureAttachments.pageSource(driver);
}
}
Environment and history¶
target/allure-results/environment.properties:
Browser=Chrome 126
Browser.Headless=true
OS=macOS 14.5
Base.URL=https://staging.example.com
Java=21.0.3
Build=1042
To get trend graphs, copy the previous report's history/ folder into the
new allure-results/ before generating. Without that step every report looks
like the first run ever — the most common Allure disappointment.
3. ExtentReports — self-contained HTML¶
Extent produces one HTML file with no separate CLI, which suits teams that email reports around.
<dependency>
<groupId>com.aventstack</groupId>
<artifactId>extentreports</artifactId>
<version>5.1.2</version>
<scope>test</scope>
</dependency>
package com.example.listeners;
import com.aventstack.extentreports.*;
import com.aventstack.extentreports.reporter.ExtentSparkReporter;
import org.openqa.selenium.*;
import org.testng.*;
public class ExtentListener implements ITestListener {
private static ExtentReports extent;
private static final ThreadLocal<ExtentTest> TEST = new ThreadLocal<>();
@Override
public void onStart(ITestContext context) {
ExtentSparkReporter spark = new ExtentSparkReporter("target/extent-report.html");
spark.config().setDocumentTitle("QA Automation Report");
spark.config().setReportName("Regression Suite");
extent = new ExtentReports();
extent.attachReporter(spark);
extent.setSystemInfo("Browser", System.getProperty("browser", "chrome"));
extent.setSystemInfo("Environment", System.getProperty("base.url", "local"));
}
@Override
public void onTestStart(ITestResult result) {
TEST.set(extent.createTest(result.getMethod().getMethodName(),
result.getMethod().getDescription()));
}
@Override
public void onTestSuccess(ITestResult result) {
TEST.get().pass("Passed in " + duration(result) + " ms");
}
@Override
public void onTestFailure(ITestResult result) {
ExtentTest test = TEST.get();
test.fail(result.getThrowable());
WebDriver driver = com.example.driver.DriverFactory.get();
if (driver instanceof TakesScreenshot shooter) {
String base64 = shooter.getScreenshotAs(OutputType.BASE64);
test.addScreenCaptureFromBase64String(base64, "Failure screenshot");
}
}
@Override
public void onTestSkipped(ITestResult result) {
TEST.get().skip("Skipped: " + result.getThrowable());
}
@Override
public void onFinish(ITestContext context) {
extent.flush(); // WITHOUT THIS THE FILE IS EMPTY
TEST.remove();
}
}
Regression Suite -- 42 tests, 40 passed, 1 failed, 1 skipped (4m 12s)
Report written to target/extent-report.html
extent.flush() and ThreadLocal are both mandatory
Extent buffers everything in memory; without flush() in onFinish the
HTML file is created but empty. And a plain ExtentTest field is
overwritten by whichever thread runs next under parallel="methods",
scrambling logs between tests. ThreadLocal — the same pattern as the
driver in Module 03 — is the fix.
Embedding screenshots as base64 keeps the report to one file, at the cost of size; a 200-test run with failures can produce a 30 MB HTML. Write PNGs to disk and link them relatively when that becomes a problem.
4. Comparing the options¶
| Surefire HTML | Allure | ExtentReports | |
|---|---|---|---|
| Extra dependency | none | agent + CLI | one JAR |
| Output | HTML from XML | JSON → HTML | single HTML |
| Steps | no | yes (@Step) |
yes (manual log) |
| Screenshots | no | yes | yes (base64 or file) |
| History/trends | no | yes (with history/) |
no |
| Requirement links | no | @Link, @Issue, @TmsLink |
manual |
| Best for | CI ingestion | team dashboards | emailing one file |
5. What a report must contain¶
Reporting tools make it easy to produce something pretty and useless. A report that serves the Level 1 execution-summary purpose needs:
- Pass/fail/skip counts and the total planned, so gaps are visible.
- Environment: browser, version, base URL, build number, timestamp.
- For every failure: the assertion message, a screenshot, and the step it failed at.
- Duration per test, so the slowest ones are identifiable.
- A link from each test to the requirement or ticket it covers.
- Trend across recent runs — one failure is noise; four in a row is a defect.
If your report cannot answer "can we ship?", it is decoration.
6. Testing traps¶
Trap 1 — a report that hides skips
A suite reporting "40 passed" out of 60 written tests is a red build dressed as green. Always show planned vs executed.
Trap 2 — screenshots taken after teardown
If @AfterMethod quits the driver before the listener's
onTestFailure runs, the screenshot call throws
NoSuchSessionException. TestNG runs listeners around configuration
methods; verify the ordering on your setup rather than assuming it.
Trap 3 — the report as the only artifact
HTML in target/ is deleted by the next mvn clean. Archive reports as
CI build artifacts, or the evidence for the release you shipped last
Tuesday no longer exists.
Trap 4 — logging secrets into the report
@Step("Log in as {0}/{1}") prints the password into HTML that gets
emailed around. Mask credentials in step names and attachments.
Trap 5 — the missing AspectJ weaver
Allure's @Step and @Attachment annotations do nothing without the
aspectjweaver -javaagent in argLine. The symptom is a report that
generates cleanly with no steps in it — and no error message anywhere.
Cheat sheet¶
| Task | Command / code |
|---|---|
| Raw XML results | target/surefire-reports/, target/failsafe-reports/ |
| Plain HTML report | mvn surefire-report:report |
| Allure: view | allure serve target/allure-results |
| Allure: generate | allure generate target/allure-results -o target/allure-report --clean |
| Allure step | @Step("Submit credentials for {0}") |
| Allure attachment | @Attachment(value="Screenshot", type="image/png") |
| Allure runtime attach | Allure.addAttachment(name, "image/png", stream, ".png") |
| Allure metadata | @Epic, @Feature, @Story, @Severity, @Link, @Issue |
| Allure environment | target/allure-results/environment.properties |
| Extent init | new ExtentSparkReporter("target/extent-report.html") |
| Extent per-test | extent.createTest(name) in a ThreadLocal |
| Extent screenshot | test.addScreenCaptureFromBase64String(base64, "caption") |
| Extent finalise | extent.flush() in onFinish |
How It Actually Works¶
Allure's step-by-step, screenshot-rich report isn't produced by re-running
your tests through a special "reporting mode" — it works via a
listener/agent hooked into the same lifecycle events every runner
already fires. allure-testng/allure-junit5 register a listener that
implements TestNG's ITestListener (or JUnit 5's TestExecutionListener)
interface, so every lifecycle callback the framework already calls
internally — onTestStart, onTestSuccess, onTestFailure — is also
delivered to Allure's listener. On each callback, the listener doesn't
generate HTML immediately; it writes a small JSON file per test (and per
"step," if you annotate methods with @Step) into target/allure-results/
containing status, timings, parameters and any attachments — a screenshot
you capture in an @AfterMethod failure hook and attach via
Allure.addAttachment(...) is simply saved as a file referenced from that
JSON. The separate allure generate/allure serve command is a static
site generator: it reads every JSON result file after the run has fully
finished, aggregates them into trend data and a navigable tree, and emits
a self-contained HTML/JS bundle — which is why Allure results and Allure
reports are two distinct artifacts, and why CI pipelines archive the
allure-results JSON directory (small, one per test) rather than the
generated site (large, and reproducible from the JSON at any time).
The Surefire/Failsafe XML underneath all of this predates Allure by
years and follows the de facto "JUnit XML" schema — <testsuite> /
<testcase> elements with <failure>/<error> children — which is why
virtually every CI system and reporting tool can consume it without any
tool-specific plugin: it's a common interchange format each tool reads
independently, not something Allure or Jenkins invented.
Exercise¶
- Run
mvn clean verifythenmvn surefire-report:report, open the HTML, and note the three things it does not tell you that a release manager would ask. - Add Allure to your Module 03 TestNG suite, annotate one test with
@Epic,@Feature,@Story,@Severityand@Link, and generate the report. - Convert three private helper methods into
@Stepmethods with parameters in the step name. Break a test and confirm the report shows which step failed. Then remove theaspectjweaver-javaagentline, regenerate, and record what changes — this is trap 5, and recognising it saves an hour. - Attach a screenshot and the page source on failure, using the listener from Module 03.
- Write
environment.propertieswith browser, OS, base URL and build number, and confirm the Environment panel populates. - Run the suite twice, copying
history/between runs, and confirm the trend graph appears. - Implement
ExtentListeneras in section 3 with aThreadLocal. Then replace theThreadLocalwith a plain field, run withparallel="methods" thread-count="3", and describe exactly how the report is wrong. - Using your generated report, write the same execution summary you wrote by hand in the Level 1 project. Note which numbers you could copy directly and which you still had to work out yourself.