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 ExtentReportsC# 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.