skip to content

The Maven Surefire report schema `surefire-test-report.xsd` declares `<testsuite>` as its only root element, yet many JUnit-XML result files use a `<testsuites>` root holding several suites. What is going on, and what must a reader of these files do about it?

level: middleimportance: must knowfreq 60%

answer

  1. one format, no owner
  2. the only published schema is a vendor's
  3. singular root versus plural wrapper
  4. readers dispatch on the root element name

basics

~20 s

No single official schema governs this format. The only published one, surefire-test-report.xsd, has a single root, testsuite, and declares no testsuites wrapper; other writers emit that wrapper anyway, so readers dispatch on whichever root they find.

solid answer

~40 s

There is no standards body behind the JUnit-XML result format. The one published schema in the family, Maven Surefire's `surefire-test-report.xsd` (`version="3.0.2"`), declares a single root element, `<testsuite>`, and no `<testsuites>` wrapper at all; it has no `targetNamespace` either, which is why the writer points at it with `xsi:noNamespaceSchemaLocation`. JUnit 5's `LegacyXmlReportGeneratingListener` also roots each file at `<testsuite>`, writing `TEST-<rootName>.xml`. But plenty of other producers pack many suites into one document under a `<testsuites>` root, and that shape is describable by nothing here. Readers therefore give up on validating and dispatch on the root element name: Allure 2's `JunitXmlPlugin` parses a `<testsuite>` root directly, flattens a `<testsuites>` root to its `<testsuite>` children, and logs and skips anything else. Allure 3's junitxml reader accepts both roots too.

code

xml · 10 lines
xml
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
  <testsuite name="com.example.CartTest" tests="2" errors="0" skipped="0" failures="0">
    <testcase name="addsItem" classname="com.example.CartTest" time="0.014"/>
    <testcase name="clearsCart" classname="com.example.CartTest" time="0.007"/>
  </testsuite>
  <testsuite name="com.example.OrderTest" tests="1" errors="0" skipped="0" failures="0">
    <testcase name="totalsLines" classname="com.example.OrderTest" time="0.031"/>
  </testsuite>
</testsuites>

go deeper

for a junior

Know that this file is a de-facto format with no standards body behind it, and that some producers write one suite per file while others pack several suites into a single document.

for a middle

Explain that the only published schema here is Maven Surefire's, that its root is the singular testsuite element with no testsuites wrapper, and that readers dispatch on whichever root they actually find.

for a senior

Be ready to debug a consumer that silently drops files. Show how you would confirm the root element and attribute set a producer really emits before you start blaming the reader.

for a principal

Own the decision about what your organisation treats as the contract here: the published schema, the intersection of what your tools demonstrably read, or a checked-in sample — and say who maintains it.

## A format with no owner The file at the centre of this is usually called *the JUnit XML report*, but no organisation publishes it and no version of JUnit ever defined it. It began as the output of one build tool's test task and spread by imitation: every runner that wanted its results to show up in a CI dashboard copied the element names it saw, and every dashboard that wanted to ingest results learned to read whatever the runners it cared about produced. There is no registry, no namespace, and no conformance suite. What exists instead is a **family of dialects** that agree on the obvious parts and diverge wherever the original output happened to be silent. The practical consequence is the one that catches people out: you cannot look up *the* spec and be done. The nearest thing to a spec is one vendor's schema, and it describes that vendor's output. ## What the one published schema actually says Maven Surefire ships `surefire-test-report.xsd` and stamps `version="3.0.2"` on it. Three things about it matter here. - **Its root element is `<testsuite>`, singular.** The schema declares no `<testsuites>` wrapper anywhere — not as a root, not as an optional container. A document whose outermost element is `<testsuites>` is not describable by this schema at all. - **It has no `targetNamespace`.** The elements are unqualified, which is exactly why the writer points at the schema with `xsi:noNamespaceSchemaLocation` rather than by declaring a namespace. - **It covers one suite per document.** Surefire writes one file per source class, named `TEST-<sourceName>.xml`, so a run leaves behind a *directory* of files that a consumer globs, not one document it opens. JUnit 5's own `LegacyXmlReportGeneratingListener` agrees on the root: it writes `<testsuite>` too, one file per root, named `TEST-<rootName>.xml`. So the two most common writers in this ecosystem both produce single-suite documents. ## The dialect the schema does not cover And yet `<testsuites>` documents are everywhere: runners from other ecosystems, format converters, and scripts that merge a sharded run into one artefact all commonly put several `<testsuite>` elements under a single `<testsuites>` root. Nothing forbids that, because there is nothing here to do the forbidding. It is simply not the shape the published schema describes. | producer | root it writes | describable by `surefire-test-report.xsd` | |---|---|---| | Maven Surefire | `<testsuite>`, one file per class | yes — it is that schema's own output | | JUnit 5's `LegacyXmlReportGeneratingListener` | `<testsuite>`, one file per root | no — it adds attributes the schema omits | | merge scripts and non-Java runners | `<testsuites>` wrapping many `<testsuite>` | no — the root is not declared at all | ## How readers cope: dispatch, not validate Because both shapes are in circulation, a consumer that wants to be useful cannot insist on one of them. The readers in this family all do the same four things: 1. Parse the document and take the **name of its root element**. 2. If it is `testsuite`, treat the whole document as one suite. 3. If it is `testsuites`, treat each `<testsuite>` **child** as a suite and process them in turn. 4. Otherwise, record that the file is not recognisable JUnit XML and move on. Allure 2's `JunitXmlPlugin` is precisely this, and its fourth branch is the one worth dwelling on: an unrecognised root produces a **debug log line and a silent skip**, not an error and not a failed build. Allure 3's junitxml reader handles both roots as well. So a file with the wrong root does not break anything — it just is not in the report, and nobody is told why. Note also that the flattening is **one level deep**. A `<testsuites>` root is expanded to its immediate `<testsuite>` children; nested wrappers are not walked recursively, so a document that nests suites inside suites loses everything below the first level. ## What this means when you write or wire a consumer - **Never assume validation happens.** `xsi:noNamespaceSchemaLocation` is a hint for a validating parser, not an instruction. Nothing in the normal chain switches validation on. - **Handle both roots from the first commit.** It costs four lines and it is the single most common reason a home-grown ingester works on one team's artefacts and not another's. - **Expect the absence, not the error.** When results are missing from a report, the root element is the first thing to look at: a wrong root is silent, and silence looks exactly like "the tests did not run". - **Pin what you rely on with a sample.** Since no schema is authoritative, the only durable statement of the contract is a checked-in example file plus the set of names your consumer actually looks up.

  • What does Allure 2's `JunitXmlPlugin` do with a file whose root element is neither `<testsuite>` nor `<testsuites>`?
    Nothing useful: after parsing, it compares the root element's name against those two, and when neither matches it logs the file as not valid JUnit XML, names the unknown root, and returns. The file is silently absent from the generated report rather than failing anything.
  • Does pointing at the schema with `xsi:noNamespaceSchemaLocation` make a consumer validate the file?
    No. It is a hint telling a validating parser where a schema lives for a document that has no namespace. The readers in this family do not validate at all; they walk elements and attributes by name, so a document that would fail the schema is still consumed without complaint.

It is like a power plug that every manufacturer copied from one appliance rather than from a standard: the shape mostly matches, nobody ever published the drawing, and each new device quietly adds a pin.

saying these in an interview costs you the question

  • Believes there is an official JUnit XML standard
  • Assumes every producer wraps suites in a testsuites root
  • Expects consumers to validate the file against a schema
  • Thinks a schema-location attribute forces validation