A test suite that fails silently is almost as useless as no tests at all. Reports are how the team — developers, QA leads, and managers — understands what ran, what broke, and what was skipped. JUnit 5 integrates with three distinct reporting layers: the XML files Surefire writes automatically (read by Jenkins, GitHub Actions, and every major CI tool), the HTML report the Surefire Report Plugin generates, and Allure — the richest option — which produces interactive dashboards with timelines, attachments, and severity labels.
Surefire XML — the CI standard
Every mvn test run writes two files per test class into target/surefire-reports/:
TEST-com.mycompany.CalculatorTest.xml— standard JUnit XML consumed by CI toolscom.mycompany.CalculatorTest.txt— plain text summary for human reading
You don't configure anything for this — it happens automatically. Jenkins' JUnit plugin, GitHub Actions' test summary, GitLab's test report integration, and CircleCI's test metadata all parse this XML format. In GitHub Actions:
- name: Run tests
run: mvn test
- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always() # publish even when tests fail
with:
files: target/surefire-reports/TEST-*.xmlThe if: always() is important — without it, a test failure stops the workflow before the report is published.
Surefire Report Plugin — basic HTML
The Maven Surefire Report Plugin reads the XML files and generates a static HTML page:
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>3.2.5</version>
</plugin>
</plugins>
</reporting>Generate it:
# Run tests and generate the HTML report in one command
mvn surefire-report:report
# Or as part of the Maven site lifecycle
mvn verify siteThe report lands at target/site/surefire-report.html. It shows per-class pass/fail counts, test names, durations, and failure messages with stack traces. It is not interactive, but it requires no extra tooling and works offline.
Allure — professional interactive reports
Allure produces the kind of report that product owners can read: test names from @DisplayName, grouped by feature, with screenshots, request/response payloads, and severity ratings. It is the most widely used third-party reporting library for JUnit 5.
Add the dependency:
<dependency>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-junit5</artifactId>
<version>2.25.0</version>
<scope>test</scope>
</dependency>Add the Allure Maven plugin:
<plugin>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-maven</artifactId>
<version>2.12.0</version>
</plugin>Configure the Allure aspect agent in Surefire (required for @Step annotations):
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<argLine>-javaagent:${settings.localRepository}/org/aspectj/aspectjweaver/1.9.21/aspectjweaver-1.9.21.jar</argLine>
</configuration>
</plugin>Allure annotations in test code
import io.qameta.allure.*;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@Feature("User Login")
class LoginTest {
@Test
@Story("Successful login")
@Severity(SeverityLevel.CRITICAL)
@DisplayName("should navigate to dashboard after valid login")
void shouldLoginSuccessfully(WebDriver driver) {
openLoginPage(driver);
enterCredentials(driver, "alice@test.com", "password");
submitForm(driver);
assertEquals("Dashboard", driver.getTitle());
}
@Step("Open login page")
private void openLoginPage(WebDriver driver) {
driver.get("https://myapp.com/login");
}
@Step("Enter credentials: {email}")
private void enterCredentials(WebDriver driver, String email, String password) {
driver.findElement(By.id("email")).sendKeys(email);
driver.findElement(By.id("password")).sendKeys(password);
}
@Step("Submit the login form")
private void submitForm(WebDriver driver) {
driver.findElement(By.cssSelector("[data-testid='submit']")).click();
}
}@Feature and @Story group tests in the Allure Behaviors view. @Severity sets a colour-coded priority label. @Step breaks the test body into named steps visible in the Allure timeline — each step shows whether it passed, and for failures, exactly where the test broke.
Generate and serve the report:
mvn test # run tests, write allure-results/
allure serve target/allure-results # open browser with live reportOr generate a static HTML site:
allure generate target/allure-results --clean -o target/allure-report
allure open target/allure-reportCustom reporter with TestWatcher extension
For lightweight custom reporting without Allure — for example, a simple JSON summary or a Slack notification — implement the TestWatcher extension from Chapter 4:
import org.junit.jupiter.api.extension.*;
import java.util.*;
import java.io.*;
import java.nio.file.*;
public class JsonReporter implements TestWatcher, AfterAllCallback {
private final List<Map<String, String>> results = Collections.synchronizedList(new ArrayList<>());
@Override
public void testSuccessful(ExtensionContext ctx) {
results.add(Map.of("name", ctx.getDisplayName(), "status", "PASSED"));
}
@Override
public void testFailed(ExtensionContext ctx, Throwable cause) {
results.add(Map.of(
"name", ctx.getDisplayName(),
"status", "FAILED",
"error", cause.getMessage()
));
}
@Override
public void afterAll(ExtensionContext ctx) throws Exception {
String json = buildJson(results);
Path report = Path.of("target/test-summary.json");
Files.createDirectories(report.getParent());
Files.writeString(report, json);
System.out.println("Report written to " + report.toAbsolutePath());
}
private String buildJson(List<Map<String, String>> results) {
// Use Jackson or Gson in a real implementation
StringBuilder sb = new StringBuilder("[\n");
for (int i = 0; i < results.size(); i++) {
var r = results.get(i);
sb.append(String.format("""
{ "name": "%s", "status": "%s"%s }""",
r.get("name"), r.get("status"),
r.containsKey("error") ? ", \"error\": \"" + r.get("error") + "\"" : ""
));
if (i < results.size() - 1) sb.append(",");
sb.append("\n");
}
return sb.append("]").toString();
}
}Register globally via META-INF/services/org.junit.jupiter.api.extension.Extension so it applies to every test class without any per-class annotation.
Reporting pipeline
⚠️ Common mistakes
- Not using
if: always()in GitHub Actions. Themvn teststep exits with a non-zero code when tests fail, which stops subsequent steps from running. A report-upload step withoutif: always()therefore never runs when you need it most — when tests fail. Addif: always()to every step that uploads or publishes reports. - Running
allure serveon a CI machine without a display.allure serveopens a browser. In a headless CI environment, useallure generateto produce a static site, then upload the directory as an artifact. GitHub Actions'actions/upload-artifactcan storetarget/allure-report/and make it downloadable from the run summary. - Forgetting the AspectJ weaver for
@Step. Without the-javaagentin Surefire's<argLine>,@Stepannotations are silently ignored — test methods still run, but the Allure report shows no steps. The AspectJ weaver intercepts@Stepcalls at runtime; without it, Allure only sees the test-level events.
🎯 Practice task
Set up the full reporting pipeline. 25–35 minutes.
- Run
mvn testand opentarget/surefire-reports/. Find the XML file for one of your test classes. Read the<testcase>elements and note how JUnit 5 display names appear in the XMLnameattribute. - HTML report. Add the Surefire Report Plugin to the
<reporting>section. Runmvn surefire-report:report. Opentarget/site/surefire-report.html. Find the test you intentionally failed earlier. - Allure integration. Add
allure-junit52.25.0 to dependencies. Add@Feature("Login")and@Severity(SeverityLevel.CRITICAL)to one test class. Add@Step(...)to two helper methods. Runmvn testthenallure serve target/allure-results. Explore the Behaviors view — confirm@Featureand@Storygrouping is visible. - CI upload. Create a
.github/workflows/test.ymlwith steps: checkout → setup Java →mvn test→ uploadtarget/surefire-reports/as an artifact (useif: always()). Push and confirm the artifact appears in the Actions run even when a test fails. - Stretch — custom reporter. Implement the
JsonReportershown in the lesson. Register it globally viaMETA-INF/services. Run your suite and opentarget/test-summary.json. Confirm the file contains one entry per test with the correct status.
You have completed Chapter 5. The capstone chapter puts everything together: a real test suite from scratch with unit tests, parameterisation, extensions, Maven configuration, and a reporting pipeline.