Extent Reports Java Guide: Beautiful HTML Test Reports for Selenium
Extent Reports is one of the most widely used open-source test reporting libraries in the Java ecosystem. It generates rich HTML reports from your Selenium, TestNG, or JUnit test runs — complete with pass/fail status, screenshots, system information, and categorized logs.
This guide covers setting up Extent Reports 5.x with a Java/Selenium/TestNG stack, customizing the output, attaching screenshots on failure, and integrating the report into CI pipelines.
What Is Extent Reports?
Extent Reports is a Java library (and available for .NET) that produces self-contained HTML reports from test execution data. Unlike JUnit XML — which requires a separate rendering tool — Extent Reports generates a standalone HTML file you can open in any browser without a server.
Key features:
- Spark reporter — responsive HTML with charts and status breakdown
- PDF reporter — printable summary for stakeholders
- Klov reporter — sends data to MongoDB for trend dashboards
- Log levels — pass, fail, skip, info, warning — attached to each test step
- Screenshots — embedded in the HTML report as base64 images
- System info — OS, browser, environment details in the report header
- Tagging and categorization — group tests by feature, module, or label
Setup
Maven Dependency
Add Extent Reports 5.x to your pom.xml:
<dependency>
<groupId>com.aventstack</groupId>
<artifactId>extentreports</artifactId>
<version>5.1.1</version>
</dependency>For TestNG integration, also add:
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.8.0</version>
<scope>test</scope>
</dependency>Basic Report Setup
The minimum code to create a report:
import com.aventstack.extentreports.ExtentReports;
import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.reporter.ExtentSparkReporter;
public class ReportManager {
private static ExtentReports extent;
private static ExtentSparkReporter sparkReporter;
public static ExtentReports getInstance() {
if (extent == null) {
sparkReporter = new ExtentSparkReporter("test-output/ExtentReport.html");
sparkReporter.config().setDocumentTitle("Automation Test Report");
sparkReporter.config().setReportName("Selenium Test Suite");
extent = new ExtentReports();
extent.attachReporter(sparkReporter);
extent.setSystemInfo("OS", System.getProperty("os.name"));
extent.setSystemInfo("Java Version", System.getProperty("java.version"));
extent.setSystemInfo("Browser", "Chrome");
extent.setSystemInfo("Environment", "QA");
}
return extent;
}
}Integration with TestNG
The cleanest approach for TestNG is implementing ITestListener to hook into TestNG's lifecycle events automatically.
TestNG Listener
import com.aventstack.extentreports.ExtentReports;
import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.Status;
import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.ITestResult;
public class ExtentTestNGListener implements ITestListener {
private static ExtentReports extent = ReportManager.getInstance();
private static ThreadLocal<ExtentTest> extentTest = new ThreadLocal<>();
@Override
public void onTestStart(ITestResult result) {
ExtentTest test = extent.createTest(result.getMethod().getMethodName());
extentTest.set(test);
}
@Override
public void onTestSuccess(ITestResult result) {
extentTest.get().log(Status.PASS, "Test passed");
}
@Override
public void onTestFailure(ITestResult result) {
extentTest.get().log(Status.FAIL, result.getThrowable());
// Attach screenshot on failure
try {
String screenshotPath = ScreenshotUtil.capture(result.getMethod().getMethodName());
extentTest.get().addScreenCaptureFromPath(screenshotPath);
} catch (Exception e) {
extentTest.get().log(Status.WARNING, "Could not capture screenshot: " + e.getMessage());
}
}
@Override
public void onTestSkipped(ITestResult result) {
extentTest.get().log(Status.SKIP, "Test skipped: " + result.getThrowable());
}
@Override
public void onFinish(ITestContext context) {
extent.flush();
}
}Register the Listener
In testng.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Automation Suite">
<listeners>
<listener class-name="com.yourpackage.ExtentTestNGListener"/>
</listeners>
<test name="Smoke Tests">
<classes>
<class name="com.yourpackage.tests.LoginTest"/>
<class name="com.yourpackage.tests.CheckoutTest"/>
</classes>
</test>
</suite>Screenshot Capture Utility
Screenshots are what make Extent Reports actually useful for debugging. Here's a utility class:
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ScreenshotUtil {
private static WebDriver driver;
public static void setDriver(WebDriver webDriver) {
driver = webDriver;
}
public static String capture(String testName) throws Exception {
TakesScreenshot ts = (TakesScreenshot) driver;
File source = ts.getScreenshotAs(OutputType.FILE);
String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String destination = "test-output/screenshots/" + testName + "_" + timestamp + ".png";
File destFile = new File(destination);
destFile.getParentFile().mkdirs();
FileUtils.copyFile(source, destFile);
return destination;
}
}For embedding screenshots as base64 (no file dependency):
public static String captureBase64() {
TakesScreenshot ts = (TakesScreenshot) driver;
return ts.getScreenshotAs(OutputType.BASE64);
}
// In your listener:
extentTest.get().addScreenCaptureFromBase64String(ScreenshotUtil.captureBase64());Adding Detailed Step Logs
Beyond pass/fail status, Extent Reports lets you log individual steps within a test:
@Test
public void loginTest() {
ExtentTest test = ReportManager.getInstance()
.createTest("Login Test", "Validates user login with valid credentials");
test.log(Status.INFO, "Navigating to login page");
driver.get("https://example.com/login");
test.log(Status.INFO, "Entering username");
driver.findElement(By.id("username")).sendKeys("testuser");
test.log(Status.INFO, "Entering password");
driver.findElement(By.id("password")).sendKeys("password123");
test.log(Status.INFO, "Clicking login button");
driver.findElement(By.id("login-btn")).click();
String currentUrl = driver.getCurrentUrl();
if (currentUrl.contains("/dashboard")) {
test.log(Status.PASS, "Login successful - redirected to dashboard");
} else {
test.log(Status.FAIL, "Login failed - unexpected URL: " + currentUrl);
}
}Categorizing Tests
Extent Reports supports assigning categories, authors, and devices to tests. This enables filtering in the report:
ExtentTest test = extent.createTest("Checkout Test")
.assignCategory("E2E", "Checkout")
.assignAuthor("QA Team")
.assignDevice("Chrome/Windows");In the Spark report, you can then filter by category to see all checkout-related tests, or by author to view a team member's test results.
Configuring the Spark Reporter
The Spark reporter accepts several configuration options:
ExtentSparkReporter sparkReporter = new ExtentSparkReporter("test-output/report.html");
sparkReporter.config().setDocumentTitle("My Test Suite");
sparkReporter.config().setReportName("Sprint 42 Regression");
sparkReporter.config().setTheme(Theme.DARK); // DARK or STANDARD
sparkReporter.config().setTimeStampFormat("MMM dd, yyyy HH:mm:ss");
sparkReporter.config().setEncoding("utf-8");
sparkReporter.config().setOfflineMode(true); // embed all assets in HTMLsetOfflineMode(true) is important for CI pipelines where the report will be viewed in environments without internet access. It embeds Bootstrap and other dependencies into the HTML file.
CI/CD Integration
Jenkins
In your Jenkins pipeline, publish the HTML report using the HTML Publisher plugin:
post {
always {
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'test-output',
reportFiles: 'ExtentReport.html',
reportName: 'Extent HTML Report'
])
}
}GitHub Actions
- name: Run tests
run: mvn test
- name: Upload Extent Report
uses: actions/upload-artifact@v3
if: always()
with:
name: extent-report
path: test-output/ExtentReport.htmlParallel Test Execution
For parallel test runs with TestNG, the ThreadLocal<ExtentTest> pattern ensures each thread gets its own test object:
private static ThreadLocal<ExtentTest> extentTest = new ThreadLocal<>();
// In onTestStart:
ExtentTest test = extent.createTest(result.getMethod().getMethodName());
extentTest.set(test);
// Access from test:
public static ExtentTest getTest() {
return extentTest.get();
}Each thread updates its own ExtentTest instance, preventing race conditions in the report data.
Common Issues
Report not generating: Call extent.flush() at the end of the suite. Without it, data in memory is never written to disk.
Screenshots not appearing: Check that the file path is relative to where the report HTML sits, or use base64 embedding to avoid path issues entirely.
Empty report on CI: Ensure test-output/ directory is writable. Some CI environments have restricted temp directories.
Duplicate test entries in parallel runs: If you're not using ThreadLocal, multiple threads will write to the same ExtentTest instance. Use the ThreadLocal pattern described above.
Extent Reports vs Allure
| Feature | Extent Reports | Allure |
|---|---|---|
| Setup complexity | Low | Medium |
| Report appearance | Good | Excellent |
| Step logging | Manual API calls | Annotation-based |
| Historical trends | Via Klov (MongoDB) | Built-in with server |
| CI artifact support | HTML file | Needs Allure server |
| Screenshot support | Built-in | Plugin |
| Java focus | Yes | Multi-language |
Allure produces more polished reports and has better historical trend support. Extent Reports is simpler to set up and works well when you need a standalone HTML artifact.
Summary
Extent Reports remains a practical choice for Java/Selenium teams that want rich HTML test reports without a separate reporting server. The listener-based TestNG integration keeps test code clean, the offline mode produces portable report files for CI artifact storage, and the screenshot API gives enough context to debug failures without reproducing them locally.
For teams already invested in Java and Selenium, adding Extent Reports is a 30-minute investment that pays off every time you need to share test results with stakeholders who don't have access to your CI system.