The TestNG default report tells you what passed and what failed. ExtentReports and Allure tell you why it looks the way it does — with screenshots embedded beside failure messages, charts showing pass rates, historical trends across CI runs, and a UI polished enough to send directly to a product team. This lesson wires up both integrations: ExtentReports as a listener that produces a single self-contained HTML file, and Allure as an annotation-driven framework that generates a rich server-rendered dashboard. Pick one — both are excellent. ExtentReports is the simpler integration; Allure has a steeper setup but richer features when you need trend history.
ExtentReports via ITestListener
Add the dependency:
<dependency>
<groupId>com.aventstack</groupId>
<artifactId>extentreports</artifactId>
<version>5.1.1</version>
<scope>test</scope>
</dependency>The listener creates one ExtentTest entry per test method, logs pass/fail/skip, and attaches screenshots on failure. ThreadLocal<ExtentTest> is critical for parallel correctness — each thread writes to its own entry:
package com.mycompany.tests.listener;
import com.aventstack.extentreports.ExtentReports;
import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.reporter.ExtentSparkReporter;
import com.mycompany.tests.util.DriverManager;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.testng.*;
import java.io.*;
import java.nio.file.*;
public class ExtentReportListener implements ITestListener {
private static ExtentReports extent;
private static final ThreadLocal<ExtentTest> testEntry = new ThreadLocal<>();
@Override
public void onStart(ITestContext context) {
ExtentSparkReporter spark = new ExtentSparkReporter("reports/extent-report.html");
spark.config().setDocumentTitle("Test Report — " + context.getName());
spark.config().setReportName(context.getName());
extent = new ExtentReports();
extent.setSystemInfo("Environment", System.getProperty("env", "staging"));
extent.setSystemInfo("Browser", System.getProperty("browser", "chrome"));
extent.attachReporter(spark);
}
@Override
public void onTestStart(ITestResult result) {
ExtentTest test = extent.createTest(result.getName(),
result.getMethod().getDescription());
testEntry.set(test);
}
@Override
public void onTestSuccess(ITestResult result) {
testEntry.get().pass("Test passed in "
+ (result.getEndMillis() - result.getStartMillis()) + "ms");
}
@Override
public void onTestFailure(ITestResult result) {
ExtentTest test = testEntry.get();
test.fail(result.getThrowable());
// Attach screenshot if a WebDriver is available
WebDriver driver = DriverManager.getDriver();
if (driver instanceof TakesScreenshot ts) {
try {
File src = ts.getScreenshotAs(OutputType.FILE);
String dest = "reports/screenshots/"
+ result.getName() + "_" + System.currentTimeMillis() + ".png";
Files.createDirectories(Paths.get("reports/screenshots"));
Files.copy(src.toPath(), Paths.get(dest),
StandardCopyOption.REPLACE_EXISTING);
test.addScreenCaptureFromPath("screenshots/"
+ Paths.get(dest).getFileName());
} catch (IOException e) {
test.warning("Screenshot failed: " + e.getMessage());
}
}
}
@Override
public void onTestSkipped(ITestResult result) {
testEntry.get().skip(result.getThrowable() != null
? result.getThrowable().getMessage()
: "Skipped");
}
@Override
public void onFinish(ITestContext context) {
if (extent != null) extent.flush();
}
}Register in testng.xml:
<listeners>
<listener class-name="com.mycompany.tests.listener.ExtentReportListener"/>
</listeners>After mvn test, open reports/extent-report.html. It includes: a dashboard with donut charts for pass/fail/skip, a timeline of tests ordered by execution time, expandable detail per test with the failure screenshot embedded inline, and system info showing environment and browser. The file is self-contained — send it directly without a server.
Allure integration
Allure requires two pieces: a dependency that collects results during the run, and the allure CLI (or Maven plugin) that generates the HTML report from those results.
<dependency>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-testng</artifactId>
<version>2.25.0</version>
<scope>test</scope>
</dependency>No listener code required. Allure integrates at the TestNG SPI level — it captures results automatically once the dependency is on the classpath. Test results are written to allure-results/ as JSON files during the run.
Enrich the report with Allure annotations:
import io.qameta.allure.*;
import org.testng.Assert;
import org.testng.annotations.Test;
@Feature("User Authentication")
public class LoginTest extends com.mycompany.tests.base.BaseTest {
@Test(groups = {"smoke"})
@Story("Successful login")
@Severity(SeverityLevel.CRITICAL)
@Description("Valid credentials navigate the user to the inventory page")
@Owner("alice")
public void validLoginSucceeds() {
loginStep("standard_user", "secret_sauce");
Assert.assertTrue(driver().getCurrentUrl().contains("/inventory.html"));
}
@Step("Enter {email} / {password} and click login")
private void loginStep(String email, String password) {
driver().findElement(org.openqa.selenium.By.id("user-name")).sendKeys(email);
driver().findElement(org.openqa.selenium.By.id("password")).sendKeys(password);
driver().findElement(org.openqa.selenium.By.id("login-button")).click();
}
}After mvn test, generate the HTML report:
# Install allure CLI (once): brew install allure
allure serve allure-results
# Or via Maven plugin (no CLI install needed):
mvn allure:serveThe Allure dashboard shows: a timeline view of tests across threads, a Behaviours tab organised by @Feature / @Story, a test body showing each @Step that ran, screenshots attached on failure, and — over multiple CI runs — a trend graph of pass rates.
For CI, add the Maven plugin:
<plugin>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-maven</artifactId>
<version>2.12.0</version>
</plugin>mvn clean test allure:report # generates target/site/allure-maven-plugin/Choosing between ExtentReports and Allure
Reporting options at a glance
Default TestNG
Zero configuration — free
emailable-report.html works standalone
No screenshots, no charts
No history across runs
Good enough for solo or small projects
Best for: getting started fast
ExtentReports
One-file HTML — no server needed
Embedded screenshots beside failures
Charts: donut, timeline
System info: env, browser, OS
No built-in trend history
Best for: self-contained rich HTML
Allure
Requires allure CLI or Maven plugin
@Step annotation shows test body as steps
History + trends across CI runs
@Feature / @Story BDD-style grouping
Jenkins / GitHub Actions plugins available
Best for: enterprise, multi-run trends
⚠️ Common mistakes
- Not using
ThreadLocal<ExtentTest>in a parallel suite. Two threads callingextent.createTest()and then sharing a singleExtentTestfield will interleave log entries, producing a report where one test shows another test's messages.ThreadLocal<ExtentTest>ensures each thread writes to its own entry. Always use it whenparallel="methods"is active. - Forgetting
extent.flush()inonFinish. ExtentReports writes the HTML file only whenflush()is called. IfonFinishis not implemented orflush()is missing, theextent-report.htmlfile either doesn't exist or contains partial data. Always callextent.flush()inonFinish. - Calling
allure servebefore the test run completes.allure-results/is populated during the run. If you openallure servein another terminal while tests are still running, Allure reads incomplete JSON and generates a misleading report. Wait formvn testto finish, then generate the report.
🎯 Practice task
Wire up one reporting integration. 30–40 minutes.
- ExtentReports path. Add the dependency. Implement
ExtentReportListeneras shown. Register it intestng.xml. Runmvn test. Openreports/extent-report.html— confirm the donut chart shows your pass/fail split and at least one failed test has a screenshot attached. - Deliberately fail one test. Rerun. Confirm the screenshot for that test appears embedded in the
extent-report.htmlbeside the failure message. - Allure path. Remove ExtentReports or keep both. Add
allure-testngdependency. Add@Feature,@Story, and@Stepto three test methods. Runmvn test. Then runallure serve allure-results. Explore the Behaviours and Timeline tabs. - Parallel safety check. Enable
parallel="methods" thread-count="4". Run with ExtentReports. Open the report — confirm each test has only its own log messages (no cross-contamination from other threads). - Stretch — Allure in CI. Add
allure-mavenplugin topom.xml. Update your GitHub Actions workflow to runmvn clean test allure:reportand uploadtarget/site/allure-maven-plugin/as a build artefact. After a run, download and open the artefact.
Next lesson: running TestNG suites in Jenkins and GitHub Actions — the CI/CD configuration that makes your suite run on every push and delivers reports automatically.