Power Your Software Testing with AI Agents and Cloud
The Native AI-Agentic Cloud Platform to Supercharge Quality Engineering. Test Intelligently and Ship Faster.
- TestMu AI (Formerly LambdaTest)
- /
- Learning Hub
- /
- JUnit Reports: XML, HTML, and CI Test Reports Explained
JUnit Reports: XML, HTML, and CI Test Reports Explained
JUnit reports: the Surefire XML line by line, legacy XML vs Open Test Reporting, HTML reports, and publishing results in Jenkins, GitHub Actions, and GitLab.
Published on:
When a build fails at 2 a.m. on a CI agent nobody was watching, the console summary has scrolled out of reach by morning and the only durable record of what happened is the report JUnit left behind. This chapter explains what a JUnit report is, reads a real Surefire XML file attribute by attribute, compares the legacy format with the JUnit Platform's Open Test Reporting, shows how to get an HTML view, and walks through publishing results in Jenkins, GitHub Actions, and GitLab so a failure is readable without opening a log.
It follows the JUnit Parallel Testing chapter, because parallel runs are where a readable report stops being optional.
TL;DR
A JUnit report is the XML that Maven Surefire or Gradle writes after a run: one testsuite per class, one testcase per method, with failure messages inline. Every CI server reads that format, the JUnit Platform can also emit the richer Open Test Reporting XML, and mvn surefire-report:report turns the XML into HTML.
- target/surefire-reports - where Maven writes TEST-<class>.xml and a .txt summary per class; the path every CI publisher points at.
- testsuite and testcase - the two elements that carry the counts, the timings, and the failure or error text a dashboard displays.
- Open Test Reporting - the event-based format from junit-platform-reporting, enabled by one configuration parameter, with display names, tags, and host details the legacy file lacks.
- CI publishers - Jenkins junit step, GitHub Actions test-reporter, GitLab artifacts:reports:junit; all three read the same XML.
- Many machines, one report - TestMu AI HyperExecute merges the Surefire XML from every parallel VM into a single job report and keeps retried attempts out of the totals.
What Is a JUnit Report?
JUnit itself produces events, not files. As the What Is JUnit chapter describes, the Platform publishes a start, pass, fail, or skip event for every container and test, and a listener turns those events into output. The listeners that matter in practice:
- The build tool's own listener - Maven Surefire and Gradle each write the familiar XML, one file per test class, in a format that predates JUnit 5 and that every CI server understands.
- The JUnit Platform's reporting module - junit-platform-reporting can write a legacy-format XML file per engine, or the newer Open Test Reporting XML with far more detail.
- The console summary - the per-class "Tests run: 3, Failures: 0" lines the build tool prints, which summarize the same events and are not a separate report.
The XML file is the one every other tool consumes. It is what a CI server parses to draw a trend graph, what a merge request shows as a test summary, and what a cloud grid merges across machines. The Jenkins JUnit plugin, whose whole job is to consume those files, is installed on 96.5% of Jenkins controllers according to the plugin site, which says how standard the format has become beyond Java.
Surefire XML Report Anatomy
The file below is TEST-demo.CalculatorTest.xml exactly as Surefire 3.6.0 wrote it when I ran the tutorial's three-test Calculator class on a TestMu AI HyperExecute Linux VM (job 4b625bc7, Java 17.0.16). The 52 property elements that record the JVM's system properties are cut; nothing else is:
<?xml version="1.0" encoding="UTF-8"?>
<testsuite xsi:noNamespaceSchemaLocation="https://maven.apache.org/surefire/maven-surefire-plugin/xsd/surefire-test-report.xsd"
version="3.0.2" name="demo.CalculatorTest" time="0.08"
tests="3" errors="0" skipped="0" failures="0" flakes="0">
<properties>
<!-- 52 <property name="..." value="..."/> elements: java.version, os.name, user.dir, ... -->
</properties>
<testcase name="rejectsZeroDivisor" classname="demo.CalculatorTest" time="0.042"/>
<testcase name="addsTwoNumbers" classname="demo.CalculatorTest" time="0.002"/>
<testcase name="dividesTwoNumbers" classname="demo.CalculatorTest" time="0.002"/>
</testsuite>| Element or attribute | Meaning | What reads it |
|---|---|---|
| testsuite name, time | The test class and its total wall-clock seconds. | Every dashboard; the slowest-class view in Jenkins sorts on time. |
| tests, failures, errors, skipped | Counts per class. A failed assertion is a failure; any other exception is an error; @Disabled and aborted assumptions are skipped. | Build status: GitLab and Jenkins mark the job unstable or failed from these four numbers. |
| flakes | Tests that failed and then passed on a Surefire rerun (rerunFailingTestsCount); part of the Surefire 3 report schema. | Rarely surfaced by CI, which is why a green build can hide flaky tests. |
| properties | The JVM system properties at run time: Java version, OS, user.dir, Maven settings. | Debugging "works on my machine": compare java.version between two reports. |
| testcase name, classname, time | One per test method. For @ParameterizedTest the name carries the invocation index and arguments. | Per-test history and the per-test duration charts. |
| failure or error child | Present only when the test did not pass: a message attribute, a type attribute with the exception class, and the stack trace as text. | The text a CI server shows when you click a red test. |
| skipped child | Present for a disabled or aborted test, with the @Disabled reason as its message. | Skipped counts and reasons in the test view. |
| system-out, system-err | Captured console output of the class, when Surefire is configured to keep it. | Jenkins shows it under the test; keepLongStdio stops long output being truncated. |
The names in that file are method names, not the @DisplayName values; "divide() rejects a zero divisor" appears only in the Open Test Reporting file below, so a team that relies on display names for readable CI output needs the newer format or an IDE. And the schema reference points at Surefire's own XSD, because there is no official JUnit XML standard: Surefire's schema is the de facto one that other tools, including pytest and Playwright reporters, imitate.
Legacy XML vs Open Test Reporting
The JUnit Platform Reporting page in the user guide documents two formats from the junit-platform-reporting artifact. LegacyXmlReportGeneratingListener writes the Surefire-compatible format, one file per engine, and is what the Console Launcher uses. OpenTestReportGeneratingListener writes Open Test Reporting XML and is auto-registered once the artifact is on the test classpath, but it stays off until you enable it.
| Legacy XML (Surefire style) | Open Test Reporting | |
|---|---|---|
| Shape | One testsuite per class, flat testcase list | A stream of started and finished events with parent IDs, so nested classes, dynamic tests, and suites keep their tree |
| Names | Method and class names | Display names, unique IDs, legacy names, and tags as metadata |
| Environment | JVM system properties | An infrastructure block: host name, user, OS, CPU cores, Java version, plus optional git metadata |
| Enable | Default in Surefire and Gradle | junit.platform.reporting.open.xml.enabled=true |
| Where | target/surefire-reports (Maven), build/test-results (Gradle) | junit.platform.reporting.output.dir, default target for Maven and build for Gradle |
| Read by | Every CI server | Tools that understand the opentest4j reporting schema, and the format's own CLI tool, which converts it into a readable hierarchical report |
Enabling it took two lines in src/test/resources/junit-platform.properties and the dependency from the JUnit Maven Dependency chapter's bom:
# src/test/resources/junit-platform.properties
junit.platform.reporting.open.xml.enabled=true
junit.platform.reporting.output.dir=target/open-test-reportThe same run then produced a second file. It is 3,985 bytes against the Surefire file's 7,570, because it skips the system properties, and it records what the legacy file cannot:
<?xml version="1.0" ?>
<e:events xmlns="https://schemas.opentest4j.org/reporting/core/0.2.0"
xmlns:e="https://schemas.opentest4j.org/reporting/events/0.2.0" ...>
<infrastructure><hostName>prod-hyperexecute-linux-dynamic-spot-1-v1633797</hostName><userName>ltuser</userName>
<operatingSystem>Linux</operatingSystem><cpuCores>4</cpuCores><java:javaVersion>17.0.16</java:javaVersion>...</infrastructure>
<e:started id="1" name="JUnit Jupiter" time="2026-10-07T12:01:44.429913908Z">...</e:started>
<e:started id="2" name="CalculatorTest" parentId="1" time="2026-10-07T12:01:44.461808035Z">...</e:started>
<e:started id="3" name="divide() rejects a zero divisor" parentId="2" time="2026-10-07T12:01:44.489002314Z">
<metadata><junit:uniqueId>[engine:junit-jupiter]/[class:demo.CalculatorTest]/[method:rejectsZeroDivisor()]</junit:uniqueId>...</metadata>
</e:started>
<e:finished id="3" time="2026-10-07T12:01:44.534884022Z"><result status="SUCCESSFUL"></result></e:finished>
...
<e:finished id="1" time="2026-10-07T12:01:44.556456068Z"><result status="SUCCESSFUL"></result></e:finished>
</e:events>Use the legacy file for anything a CI server will parse today, and switch the Open Test Reporting file on when you need display names, nested structure, or the host that ran a flaky test; the two coexist in the same build.
HTML Reports With the Surefire Report Plugin
Neither JUnit nor Surefire writes HTML during the test phase. The Maven Surefire Report plugin does it afterwards: it reads the XML files and generates target/reports/surefire.html, either standalone with mvn surefire-report:report or as part of mvn site when the plugin sits in the pom's reporting section:
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>3.6.0</version>
</plugin>
</plugins>
</reporting>The page it produces is a summary table, a per-package breakdown, and the failure text per test. It is enough for a nightly email and not enough for a browser suite that needs screenshots and step logs; those come from Allure or ExtentReports, or from the test platform. The TestNG side of this tutorial covers the equivalent setup in TestNG Reports in Jenkins, and coverage numbers, which are a separate report, are the subject of code coverage with Maven and JaCoCo.
Publish JUnit Reports in Jenkins, GitHub Actions, and GitLab
All three publishers take the same input, the Surefire XML, and differ only in syntax and in what they do with it. The step always runs after the tests and always runs even when they fail; a publisher that is skipped on failure is useless on exactly the build you need it for.
# Jenkins declarative pipeline (JUnit plugin)
stage('Test') {
steps { sh 'mvn -B test' }
post {
always {
junit testResults: 'target/surefire-reports/*.xml', allowEmptyResults: false
}
}
}
# GitHub Actions (dorny/test-reporter)
- name: Run tests
run: mvn -B test
- name: Publish test results
uses: dorny/test-reporter@v3
if: always()
with:
name: JUnit Results
path: target/surefire-reports/TEST-*.xml
reporter: java-junit
# GitLab CI
test:
stage: test
script:
- mvn -B test
artifacts:
when: always
reports:
junit: target/surefire-reports/TEST-*.xml| Publisher | What you get | Worth knowing |
|---|---|---|
| Jenkins JUnit plugin | Pass and fail counts per build, trend graphs, a page per failing test with message and trace. | Options such as keepLongStdio, allowEmptyResults, and skipMarkingBuildUnstable change how a failure affects the build; health scoring is tunable with healthScaleFactor. |
| GitHub Actions test-reporter | A check run on the commit with the results table and annotations on failing tests. | The action's README marks JUnit XML support as experimental, and annotations need the source tree laid out by package so stack traces can be matched to files. |
| GitLab unit test reports | A Test summary panel on the merge request showing new, fixed, and existing failures. | Per the GitLab documentation, each file must stay under 30 MB and all JUnit files in a job under 100 MB; when: always keeps a failed job's results. |
If the suite runs from a shell rather than a build tool, the Run JUnit from Command Line chapter shows the Console Launcher flags that write the legacy XML to a directory of your choice, which the same publishers then read.
Reports at Scale: Many Machines, One Result
Reports get harder the moment a suite runs on more than one machine, because each VM writes its own target/surefire-reports and nothing merges them. HyperExecute treats that as part of the run: a partialReports block in hyperexecute.yaml names the report location and framework, the platform collects the files from every task, merges them into one job report, and separates retried attempts so a pass rate is not inflated by reruns. The HyperExecute JUnit XML report documentation shows the four-line configuration:
report: true
partialReports:
frameworkName: junit
location: reports/
type: xmlThe location accepts glob wildcards, which is the fix for a report directory named with a date or an environment, and partialReports takes a list, so a job can emit JUnit XML for the CI server and an HTML report for people at the same time. Reports, logs, screenshots, and video download as one archive from the dashboard or with the CLI's --download-report and --download-artifacts flags. The job that produced the XML in this chapter finished in 42 seconds end to end, including VM provisioning and the dependency cache restore.
Note: See every JUnit result from every machine in one report on TestMu AI, with the same reports your CI already reads. Try TestMu AI free!
Troubleshooting Missing or Wrong Reports
| Symptom | Cause | Fix |
|---|---|---|
| No XML files at all | Compilation or the pre-test phase failed, so Surefire never ran. | Read the build log above the test phase; the report cannot exist for a run that did not start. |
| XML exists locally but CI shows nothing | The publisher path does not match, or the publish step runs only on success. | Point the step at target/surefire-reports/TEST-*.xml and run it with always or when: always. |
| Tests run: 0 in a class that has tests | Surefire fell back to the JUnit 4 provider because the Jupiter engine was missing. | Check for "Using auto detected provider JUnitPlatformProvider" in the log; depend on junit-jupiter, not only junit-jupiter-api. |
| Display names missing from the report | The legacy XML stores method names. | Enable Open Test Reporting or accept method names in CI. |
| Open Test Reporting file never appears | The property is set but junit-platform-reporting is not on the test classpath. | Add the artifact with test scope; the listener auto-registers only when the module is present. |
| Green build, suspicious suite | rerunFailingTestsCount hid flaky tests. | Read the flakes attribute; a non-zero value names tests that needed a retry. |
Best Practices for JUnit Reports
- Publish on every outcome - the CI publish step belongs in an always block; a report that only exists for green builds answers no questions.
- Keep the XML as a build artifact - the files are small, and a report from last Tuesday is often the fastest way to prove when a test started failing.
- Name tests for the report, not the IDE - method names reach the legacy XML; write them so a failure line reads as a sentence.
- Watch flakes as closely as failures - a retry that passes is a bug that got lucky; track the attribute or disable reruns in CI.
- Switch on Open Test Reporting for browser suites - the host and timing detail is what you need when a test fails only on one agent.
- Merge before you read - with parallel VMs, publish one merged report rather than one per machine, or the trend graphs lie.
Conclusion
Open target/surefire-reports after your next run and read one TEST-*.xml file against the table above; once the attributes make sense, every CI test view is a rendering of that JUnit report. Then add the publisher for your CI server in an always block, and enable Open Test Reporting when method names stop being enough.
The next chapter, JUnit with Selenium, moves from unit tests to browser tests, where screenshots and video join the XML as evidence. For suites that already run on many machines, the HyperExecute documentation linked above shows the one-file setup that merges the reports.
Author
Himanshu Sheth is the Director of Marketing (Technical Content) at TestMu AI, with over 8 years of hands-on experience in Selenium, Cypress, and other test automation frameworks. He has authored more than 130 technical blogs for TestMu AI, covering software testing, automation strategy, and CI/CD. At TestMu AI, he leads the technical content efforts across blogs, YouTube, and social media, while closely collaborating with contributors to enhance content quality and product feedback loops. He has done his graduation with a B.E. in Computer Engineering from Mumbai University. Before TestMu AI, Himanshu led engineering teams in embedded software domains at companies like Samsung Research, Motorola, and NXP Semiconductors. He is a core member of DZone and has been a speaker at several unconferences focused on technical writing and software quality.
Reviewer
Srinivasan Sekar is Director of Engineering at TestMu AI (formerly LambdaTest), where he leads engineering and open-source initiatives behind the Selenium and Appium automation grid and owns TestMu AI's MCP Server. A committer to Appium and a contributor to Selenium, WebdriverIO, Taiko, and AppiumTestDistribution, he brings over 15 years of experience in quality engineering and open-source technologies. He is the author of the Apress book 'The MCP Standard: A Developer's Guide to Building Universal AI Tools with the Model Context Protocol,' a Certified Kubernetes and Cloud Native Associate, and an international conference speaker. Before TestMu AI he spent over eight years at Thoughtworks as a Principal Consultant and Quality Architect. Srinivasan holds a B.Tech in Information Technology from Anna University.
JUnit Reports FAQs
Did you find this page helpful?
More Related Learning Hubs
TestMu AI forEnterprise
Get access to solutions built on Enterprise
grade security, privacy, & compliance
- Advanced access controls
- Advanced data retention rules
- Advanced Local Testing
- Premium Support options
- Early access to beta features
- Private Slack Channel
- Unlimited Manual Accessibility DevTools Tests


