# What Is JUnit XML? — The Test Format With No Specification

Canonical: https://commareports.com/glossary/junit-xml
Published: 2026-09-11

> JUnit XML is the de-facto interchange format for test results, emitted by nearly every runner and ingested by nearly every CI system. The element structure, the dialect problem, and its limits.

# JUnit XML

**JUnit XML is the de-facto interchange format for test results: an XML
document of `<testsuite>` elements containing `<testcase>` elements, each
optionally carrying a `<failure>`, `<error>` or `<skipped>` child.**
Nearly every test runner emits it; nearly every CI system ingests it.

Despite the name, it is not Java-specific. pytest (`--junitxml`), Jest,
Vitest, RSpec, `go test` via gotestsum, PHPUnit, Cypress, Playwright and
Robot Framework all produce it.

## The shape

```xml
<testsuites tests="3" failures="1" time="4.12">
  <testsuite name="checkout" tests="3" failures="1" time="4.12">
    <testcase classname="checkout.cart" name="applies discount" time="0.31"/>
    <testcase classname="checkout.cart" name="rejects expired code" time="0.44">
      <failure message="expected 400, got 200" type="AssertionError">
        traceback…
      </failure>
    </testcase>
    <testcase classname="checkout.cart" name="handles currency" time="3.37">
      <skipped/>
    </testcase>
  </testsuite>
</testsuites>
```

A `<testcase>` with no children passed. That is the whole model.

## There is no specification

The format was an output of Ant's JUnit task and was never standardised.
The consequences are practical:

- Consumers implement tolerant parsers that disagree. The same file can
  render fully in GitLab and lose the skip reasons in another system.
- Runners add non-portable attributes (`file`, `line`, `retries`,
  `flaky`) that only their own tooling reads.
- The `<testsuites>` root is optional, and some emitters omit it — which
  some parsers reject.
- There is no agreed place for attachments, screenshots or traces.

Treat it as a lowest common denominator that happens to be universal,
not as a rich format.

## What it cannot carry

The XML records that a test failed and the assertion message. It does
not usefully carry:

- Screenshots and video of the failing run.
- Playwright traces, network logs, DOM snapshots.
- Retry history, which is what identifies a
  [flaky test](/glossary/flaky-test) rather than a real one.
- Grouped console output per test.
- Any of the navigation that makes a 4,000-test run readable.

All of that lives in the runner's [HTML report](/glossary/html-report),
which is why most pipelines produce both: the XML for the gate and the
annotations, the HTML for the person diagnosing the failure.

## The usual gap

The XML gets ingested and shows up on the pull request. The HTML gets
uploaded as a [build artifact](/glossary/build-artifact), where it is
served as a zip, expires under
[retention](/glossary/artifact-retention), and is invisible to anyone
without repository access.

Publishing the HTML to a stable URL closes it: the PR annotation says
which test failed, the link next to it shows the trace and the screenshot,
and a reviewer can select the failing assertion and comment on it
directly ([comment on HTML](/comment-on-html)).

## Try it

Comma is free — unlimited reports, unlimited commenters, unlimited
revision history.

**[Publish a test report →](https://commareports.com/)**

### Related

- [Share a JUnit report](/share-junit-report) · [pytest](/share-pytest-report) · [Playwright](/share-playwright-report)
- [SARIF](/glossary/sarif) · [Flaky test](/glossary/flaky-test)
- [Report types](/report-types) · [Glossary](/glossary)
