ExtentReports and Allure Integration

9 min read

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:serve

The 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 calling extent.createTest() and then sharing a single ExtentTest field 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 when parallel="methods" is active.
  • Forgetting extent.flush() in onFinish. ExtentReports writes the HTML file only when flush() is called. If onFinish is not implemented or flush() is missing, the extent-report.html file either doesn't exist or contains partial data. Always call extent.flush() in onFinish.
  • Calling allure serve before the test run completes. allure-results/ is populated during the run. If you open allure serve in another terminal while tests are still running, Allure reads incomplete JSON and generates a misleading report. Wait for mvn test to finish, then generate the report.

🎯 Practice task

Wire up one reporting integration. 30–40 minutes.

  1. ExtentReports path. Add the dependency. Implement ExtentReportListener as shown. Register it in testng.xml. Run mvn test. Open reports/extent-report.html — confirm the donut chart shows your pass/fail split and at least one failed test has a screenshot attached.
  2. Deliberately fail one test. Rerun. Confirm the screenshot for that test appears embedded in the extent-report.html beside the failure message.
  3. Allure path. Remove ExtentReports or keep both. Add allure-testng dependency. Add @Feature, @Story, and @Step to three test methods. Run mvn test. Then run allure serve allure-results. Explore the Behaviours and Timeline tabs.
  4. 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).
  5. Stretch — Allure in CI. Add allure-maven plugin to pom.xml. Update your GitHub Actions workflow to run mvn clean test allure:report and upload target/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.

// tip to track lessons you complete and pick up where you left off across devices.