ExtentReports: Rich HTML Test Reports for Java and .NET

ExtentReports: Rich HTML Test Reports for Java and .NET

ExtentReports generates interactive HTML test reports with rich logging, screenshots, categories, and dashboards. It integrates with JUnit, TestNG, NUnit, and MSTest, making it a popular choice for teams who need more control over report content than framework-default reporters provide.

What Is ExtentReports?

ExtentReports is a test reporting library that lets test code programmatically build detailed reports. Unlike passive reporters that simply capture pass/fail, ExtentReports allows you to:

  • Add custom log messages at different levels (INFO, WARNING, PASS, FAIL)
  • Embed screenshots directly in the report
  • Organize tests by features, categories, and authors
  • Add metadata (device info, environment, build number)
  • Generate dashboard summaries with charts

The library is available for Java (Maven/Gradle) and .NET (NuGet).

Java Setup

Maven

<!-- pom.xml -->
<dependency>
    <groupId>com.aventstack</groupId>
    <artifactId>extentreports</artifactId>
    <version>5.1.1</version>
</dependency>

Gradle

dependencies {
    testImplementation 'com.aventstack:extentreports:5.1.1'
}

Basic Java Usage

import com.aventstack.extentreports.ExtentReports;
import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.reporter.ExtentSparkReporter;

public class TestBase {
    
    protected static ExtentReports extent;
    protected static ExtentTest test;
    
    @BeforeAll
    static void setupReport() {
        ExtentSparkReporter spark = new ExtentSparkReporter("reports/index.html");
        spark.config().setDocumentTitle("My Test Report");
        spark.config().setReportName("Regression Suite");
        spark.config().setTheme(Theme.DARK);
        
        extent = new ExtentReports();
        extent.attachReporter(spark);
        
        // System info appears in the report
        extent.setSystemInfo("OS", System.getProperty("os.name"));
        extent.setSystemInfo("Browser", "Chrome");
        extent.setSystemInfo("Environment", "Staging");
        extent.setSystemInfo("Build", System.getenv("BUILD_NUMBER"));
    }
    
    @AfterAll
    static void teardownReport() {
        extent.flush();  // Writes the report
    }
}
public class LoginTest extends TestBase {
    
    @Test
    void successful_login() {
        test = extent.createTest("Successful Login", "User logs in with valid credentials");
        
        test.log(Status.INFO, "Navigating to login page");
        driver.get("https://myapp.example.com/login");
        
        test.log(Status.INFO, "Entering credentials");
        driver.findElement(By.id("username")).sendKeys("alice@example.com");
        driver.findElement(By.id("password")).sendKeys("correct-password");
        driver.findElement(By.id("login-btn")).click();
        
        String expectedUrl = "https://myapp.example.com/dashboard";
        assertThat(driver.getCurrentUrl()).isEqualTo(expectedUrl);
        
        test.pass("Login successful — redirected to dashboard");
    }
    
    @Test
    void login_fails_with_wrong_password() {
        test = extent.createTest("Failed Login")
            .assignCategory("Negative Tests")
            .assignAuthor("Alice Smith");
        
        test.info("Testing with incorrect password");
        driver.get("https://myapp.example.com/login");
        driver.findElement(By.id("username")).sendKeys("alice@example.com");
        driver.findElement(By.id("password")).sendKeys("wrong-password");
        driver.findElement(By.id("login-btn")).click();
        
        WebElement error = driver.findElement(By.id("error-message"));
        assertThat(error.getText()).isEqualTo("Invalid credentials");
        
        test.pass("Error message displayed correctly: " + error.getText());
    }
}

Thread-Safe Reporting with TestNG

For parallel test execution, use ThreadLocal to avoid conflicts:

public class ExtentManager {
    
    private static ExtentReports extent;
    private static ThreadLocal<ExtentTest> extentTest = new ThreadLocal<>();
    
    public static ExtentReports getInstance() {
        if (extent == null) {
            synchronized (ExtentManager.class) {
                if (extent == null) {
                    ExtentSparkReporter spark = new ExtentSparkReporter("reports/index.html");
                    spark.config().setDocumentTitle("Parallel Test Report");
                    
                    extent = new ExtentReports();
                    extent.attachReporter(spark);
                }
            }
        }
        return extent;
    }
    
    public static ExtentTest getTest() {
        return extentTest.get();
    }
    
    public static void setTest(ExtentTest test) {
        extentTest.set(test);
    }
}
public class BaseTest {
    
    @BeforeMethod
    public void beforeTest(Method method) {
        ExtentTest test = ExtentManager.getInstance()
            .createTest(method.getName());
        ExtentManager.setTest(test);
    }
    
    @AfterMethod
    public void afterTest(ITestResult result) {
        if (result.getStatus() == ITestResult.FAILURE) {
            // Take screenshot and embed
            String screenshot = captureScreenshot(result.getName());
            try {
                ExtentManager.getTest()
                    .fail(result.getThrowable())
                    .addScreenCaptureFromPath(screenshot);
            } catch (IOException e) {
                ExtentManager.getTest().fail("Screenshot capture failed: " + e.getMessage());
            }
        } else if (result.getStatus() == ITestResult.SUCCESS) {
            ExtentManager.getTest().pass("Test passed");
        }
    }
    
    @AfterSuite
    public void afterSuite() {
        ExtentManager.getInstance().flush();
    }
    
    private String captureScreenshot(String testName) {
        TakesScreenshot ts = (TakesScreenshot) driver;
        File src = ts.getScreenshotAs(OutputType.FILE);
        String path = "reports/screenshots/" + testName + ".png";
        FileUtils.copyFile(src, new File(path));
        return path;
    }
}

Embedding Screenshots

// Embed screenshot as Base64 (no separate file needed)
test.addScreenCaptureFromBase64String(
    ((TakesScreenshot) driver).getScreenshotAs(OutputType.BASE64),
    "Homepage loaded"
);

// Embed from file path
test.addScreenCaptureFromPath("reports/screenshots/homepage.png");

// Embed in a specific log entry
test.fail("Element not found")
    .addScreenCaptureFromBase64String(screenshot);

Logging Levels

// Available log levels (shown in report with different icons/colors)
test.log(Status.INFO, "Informational message");
test.log(Status.PASS, "Step passed");
test.log(Status.FAIL, "Step failed");
test.log(Status.WARNING, "Warning message");
test.log(Status.SKIP, "Test skipped");
test.log(Status.DEBUG, "Debug message");

// Shorthand methods
test.info("Navigating to checkout page");
test.pass("Payment processed successfully");
test.fail("Checkout button not found");
test.warning("Slow response time: 3200ms");
test.skip("Test skipped - not applicable to this env");

// With markup
test.info(MarkupHelper.createLabel("Starting test", ExtentColor.BLUE));
test.pass(MarkupHelper.createLabel("PASSED", ExtentColor.GREEN));
test.fail(MarkupHelper.createCodeBlock(exception.getMessage()));

.NET Setup and Usage

NuGet

# .NET CLI
dotnet add package ExtentReports

<span class="hljs-comment"># Package Manager Console
Install-Package ExtentReports

C# with NUnit

using AventStack.ExtentReports;
using AventStack.ExtentReports.Reporter;

[TestFixture]
public class LoginTests
{
    private static ExtentReports _extent;
    private ExtentTest _test;
    
    [OneTimeSetUp]
    public void Setup()
    {
        var spark = new ExtentSparkReporter("reports/index.html");
        spark.Config.DocumentTitle = "My Test Report";
        spark.Config.ReportName = "Login Tests";
        spark.Config.Theme = Theme.Dark;
        
        _extent = new ExtentReports();
        _extent.AttachReporter(spark);
        _extent.AddSystemInfo("OS", Environment.OSVersion.ToString());
        _extent.AddSystemInfo("Browser", "Chrome");
    }
    
    [SetUp]
    public void BeforeTest()
    {
        _test = _extent.CreateTest(TestContext.CurrentContext.Test.Name);
    }
    
    [TearDown]
    public void AfterTest()
    {
        var status = TestContext.CurrentContext.Result.Outcome.Status;
        var message = TestContext.CurrentContext.Result.Message;
        
        switch (status)
        {
            case TestStatus.Failed:
                _test.Fail(message);
                var screenshot = CaptureScreenshot();
                _test.AddScreenCaptureFromBase64String(screenshot);
                break;
            case TestStatus.Passed:
                _test.Pass("Test passed");
                break;
            case TestStatus.Skipped:
                _test.Skip("Test skipped");
                break;
        }
    }
    
    [OneTimeTearDown]
    public void TearDown()
    {
        _extent.Flush();
    }
    
    [Test]
    [Category("Smoke")]
    public void SuccessfulLogin()
    {
        _test.Info("Opening login page");
        _driver.Navigate().GoToUrl("https://myapp.example.com/login");
        
        _test.Info("Entering credentials");
        _driver.FindElement(By.Id("username")).SendKeys("alice@example.com");
        _driver.FindElement(By.Id("password")).SendKeys("password123");
        _driver.FindElement(By.Id("login-btn")).Click();
        
        Assert.That(_driver.Url, Does.Contain("/dashboard"));
        _test.Pass("Login successful — on dashboard");
    }
}

C# with MSTest

[TestClass]
public class CheckoutTests
{
    private static ExtentReports _extent;
    private ExtentTest _test;
    
    [ClassInitialize]
    public static void ClassInit(TestContext context)
    {
        var spark = new ExtentSparkReporter("reports/index.html");
        _extent = new ExtentReports();
        _extent.AttachReporter(spark);
    }
    
    [TestInitialize]
    public void TestInit()
    {
        _test = _extent.CreateTest(TestContext.TestName);
    }
    
    [TestCleanup]
    public void TestCleanup()
    {
        if (TestContext.CurrentTestOutcome == UnitTestOutcome.Failed)
        {
            _test.Fail("Test failed: " + TestContext.CurrentTestOutcome);
        }
        else
        {
            _test.Pass("Test passed");
        }
    }
    
    [ClassCleanup]
    public static void ClassCleanup()
    {
        _extent.Flush();
    }
    
    [TestMethod]
    [TestCategory("Checkout")]
    public void CompletesPurchaseWithValidCard()
    {
        _test.Info("Adding item to cart");
        // ... test implementation
        _test.Pass("Order completed: " + orderId);
    }
}

Categories and Authors

Categorize tests for filtering in the report:

// Java
test = extent.createTest("My Test")
    .assignCategory("Smoke", "Login")
    .assignAuthor("Alice Smith")
    .assignDevice("Chrome 120");
// C#
_test = _extent.CreateTest("My Test");
_test.AssignCategory("Smoke", "Login");
_test.AssignAuthor("Alice Smith");

Categories appear as filter tabs in the report sidebar.

Child Tests (Steps)

Nest tests to create step hierarchies:

ExtentTest loginTest = extent.createTest("Login Workflow");

ExtentTest step1 = loginTest.createNode("Open login page");
step1.info("Navigating to /login");
driver.get("https://myapp.example.com/login");
step1.pass("Login page loaded");

ExtentTest step2 = loginTest.createNode("Enter credentials");
step2.info("Entering username and password");
// ...
step2.pass("Credentials entered");

ExtentTest step3 = loginTest.createNode("Verify redirect");
step3.info("Checking redirect to dashboard");
// ...
step3.pass("Redirected to dashboard");

Steps appear nested in the test timeline.

Multiple Reporters

ExtentReports supports multiple simultaneous reporters:

// Spark HTML reporter
ExtentSparkReporter spark = new ExtentSparkReporter("reports/index.html");

// PDF reporter
ExtentPDFReporter pdf = new ExtentPDFReporter("reports/report.pdf");

// Klov server (shared cloud-based reporting)
KlovReporter klov = new KlovReporter();
klov.initMongoDbConnection("localhost", 27017);
klov.setProjectName("My Project");
klov.setReportName("Regression");

ExtentReports extent = new ExtentReports();
extent.attachReporter(spark, pdf, klov);

CI/CD Integration

GitHub Actions

- name: Run tests
  run: mvn test -Dsurefire.failIfNoSpecifiedTests=false

- name: Upload ExtentReport
  uses: actions/upload-artifact@v4
  if: always()
  with:
    name: extent-report
    path: reports/

- name: Publish report to GitHub Pages
  if: github.ref == 'refs/heads/main' && always()
  uses: peaceiris/actions-gh-pages@v3
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    publish_dir: reports/
    destination_dir: reports/${{ github.run_id }}

Setting Report Title from CI Variables

@BeforeAll
static void setup() {
    ExtentSparkReporter spark = new ExtentSparkReporter("reports/index.html");
    
    ExtentReports extent = new ExtentReports();
    extent.setSystemInfo("Build", System.getenv("GITHUB_RUN_ID"));
    extent.setSystemInfo("Branch", System.getenv("GITHUB_REF_NAME"));
    extent.setSystemInfo("Commit", System.getenv("GITHUB_SHA"));
    extent.setSystemInfo("Triggered By", System.getenv("GITHUB_ACTOR"));
    
    extent.attachReporter(spark);
}

Comparison with Allure

Feature ExtentReports Allure
Languages Java, .NET Many
Setup Moderate Complex
Report type Self-contained HTML Static site (requires server)
Historical trends Via Klov Built-in
Screenshots
Categories
Annotations ✓ (richer)
BDD integration Limited Strong
Open source

ExtentReports is simpler to set up and produces self-contained HTML that's easy to archive and share. Allure has richer features but requires separate infrastructure for historical trends. For teams that don't need history dashboards, ExtentReports is the lower-friction choice.

ExtentReports occupies the middle ground between raw JUnit XML and full-featured platforms like ReportPortal: more than a basic reporter, less than a reporting server. For teams that want readable HTML reports without operational overhead, it's the right fit.

Read more