skip to content

Interchange Schemas

The XML result file every CI tool claims to read: what its elements and attributes actually declare, and where the format's silence lets each writer invent its own answer. Few read the real schema.

on this pageshow

explore

questions

20

In the JUnit-XML result file Maven Surefire writes, how is a passing test case recorded, and which children of `<testcase>` mark a case that did not pass?

level: juniorimportance: must knowfreq 68%

answer

  1. the verdict nobody writes down
  2. absence is the signal
  3. no outcome child means green
  4. SUCCESS maps to an empty tag name

basics

~10 s

A pass is written as a <testcase> element carrying no outcome child at all. Only <failure>, <error> and <skipped> mark a non-pass, so a reader concludes a case passed from their absence.

solid answer

~40 s

The Maven Surefire report schema gives `<testcase>` no attribute or child that says *passed*. A case that passed is simply a `<testcase>` element with none of the outcome children present -- no `<failure>`, no `<error>`, no `<skipped>`. Surefire's `ReportEntryType` enum makes that explicit: its `SUCCESS` row maps to an empty XML tag name, so there is literally no tag to write, while every other outcome names exactly one child. Readers invert the same rule; Allure 2's `JunitXmlPlugin` looks for `<failure>`, then `<error>`, then `<skipped>`, and returns `PASSED` when it finds none. Note that a pass is not necessarily childless: `<system-out>` and `<system-err>` may still be there. The rule is *no outcome child*, not *no children*.

code

xml · 11 lines
xml
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="com.example.CartTest" tests="3" errors="0" skipped="1" failures="1" time="0.412">
  <testcase name="addsItem" classname="com.example.CartTest" time="0.031"/>
  <testcase name="rejectsNegativeQuantity" classname="com.example.CartTest" time="0.010">
    <failure message="quantity was accepted" type="java.lang.AssertionError">java.lang.AssertionError: quantity was accepted
	at com.example.CartTest.rejectsNegativeQuantity(CartTest.java:42)</failure>
  </testcase>
  <testcase name="appliesCoupon" classname="com.example.CartTest" time="0.000">
    <skipped message="coupon service unavailable"/>
  </testcase>
</testsuite>

go deeper

for a junior

Be able to say plainly that a passing case is a <testcase> with no outcome child, and to name the three children that mark a non-pass: <failure>, <error> and <skipped>.

for a middle

Explain the mechanism, not just the rule: the writer's SUCCESS outcome maps to an empty tag name, so nothing is emitted, and a reader treats passed as the fallthrough when no outcome child matched.

for a senior

Show you know what the encoding costs in practice -- a truncated or killed run yields missing cases rather than incomplete ones, so a green-looking file can hide work that never happened.

for a principal

Be ready to say what your organisation treats as the contract when a format encodes its most common outcome as an absence, and where the check that everything expected actually ran has to live instead.

## The format records exceptions, not verdicts The JUnit-XML interchange file is often described as if every test case carried a verdict. It does not. In the Maven Surefire report schema `surefire-test-report.xsd`, a `<testcase>` element declares `@name` and `@time` as required and `@classname`, `@group` and `@timestamp` as optional. **None of those attributes says whether the case passed.** There is no `@status`, no `@result`, and no `<passed>` or `<success>` element anywhere in the schema. What the schema does declare, in a fixed order, is a set of *outcome children* that a case may contain. Three of them are the ordinary, non-repetition outcomes: - **`<failure>`** -- the case ended on an assertion that did not hold. - **`<error>`** -- the case ended on something that was not an assertion. - **`<skipped>`** -- the case was not executed to a verdict. A pass is the absence of all three. The writer emits the `<testcase>` element, writes its attributes, finds no outcome to record, and closes it. That is the whole mechanism. ## Where the absence comes from on the writing side Surefire keeps the mapping from an outcome to the XML tag that represents it in the enum `ReportEntryType`. Its `ERROR`, `FAILURE` and `SKIPPED` rows each name a tag -- `error`, `failure`, `skipped` -- and its `SUCCESS` row maps to an **empty string**. The serializer asks an entry for its tag only when the outcome is not `SUCCESS`; for a green case there is no tag to write, so nothing is written. The absence is not an omission or an optimisation someone bolted on -- it is the tag name itself being empty. ## What a consumer does with it Readers invert the same rule. Allure 2's `JunitXmlPlugin`, which parses these files, decides a case's status by looking for children in order: `<failure>` first, then `<error>`, then `<skipped>`. If none of the three is present -- and the case does not carry a dialect `@status` of `notrun` -- it returns `PASSED`. **Pass is the fallthrough branch**, the thing you get when nothing matched, which is exactly what the writer encoded. | What the file contains | What a reader concludes | |---|---| | `<testcase>` with `<failure>` | the case failed an assertion | | `<testcase>` with `<error>` | the case ended on a non-assertion throwable | | `<testcase>` with `<skipped>` | the case was not run to a verdict | | `<testcase>` with none of the three | the case passed | | no `<testcase>` element at all | nothing -- the reader never sees the case | ## The nuance that catches people: absent outcome, not absent children "A pass is an empty element" is *almost* right, and it is worth being exact, because a passing case can legitimately carry children. `<system-out>` and `<system-err>` are declared on `<testcase>` independently of the outcome children, and a writer may be configured to record captured output for green cases as well as red ones. So a `<testcase>` with a `<system-out>` child and nothing else is still a pass. The rule a reader must apply is therefore: 1. Look for `<failure>`, `<error>` and `<skipped>` specifically -- not for "any child". 2. Treat their combined absence as a pass. 3. Do not treat a self-closed `<testcase/>` as a special case; it is just the common shape of the same thing. ## What the design costs Encoding the majority outcome as an absence keeps the file small -- most cases in a healthy suite are green, and green costs nothing but a one-line element. The price is paid in two places. **A pass and a non-report are indistinguishable at the case level.** If a run is killed halfway, the cases that never executed do not appear as incomplete; they simply are not in the file, or the file is not written at all. Nothing inside the document distinguishes "this passed" from "this was never reached", because both are represented by the lack of a marker. Only comparing what the file contains against what was expected to run recovers that difference, and that comparison lives outside the format. **A truncated file reads as a green file.** A strict XML parser will object to an unclosed root element, but many consumers of these files are tolerant, and a document that ends early after a run of green cases looks much like a document that legitimately ended there. ## Practical takeaways - Never look for a positive pass marker in JUnit-XML; there is none to find. - When writing a converter into this format, emit no outcome child for a pass -- do not invent one, because every existing reader ignores children it does not recognise and still falls through to `PASSED`. - When writing a reader, check for the three outcome elements by name; anything else you find, including `<system-out>`, `<system-err>` and `<properties>`, is not an outcome. - Treat the counts on the enclosing suite and the per-case children as two independent statements about the same run; the file itself does not enforce that they agree.

  • If a pass is recorded as an absence, how does a reader tell a passing case from one the writer never got to?
    It cannot, from the case alone. A run killed part-way produces fewer `<testcase>` elements, or no file, rather than cases marked incomplete -- both a pass and a never-reached case are represented by the lack of a marker. Recovering the difference means comparing what the file holds against what was expected to run.
  • Can a passing `<testcase>` carry any children at all?
    Yes. `<system-out>` and `<system-err>` are declared on `<testcase>` independently of the outcome, and a writer may be configured to record captured output for green cases too. What a passing case never carries is `<failure>`, `<error>` or `<skipped>`.

It works like an attendance sheet that records only absences: a name with nothing beside it means the person turned up. Cheap to keep, but a torn-off corner of the page and a full day of attendance then look identical.

saying these in an interview costs you the question

  • Claims a pass is written as a <passed> or <success> element
  • Says every <testcase> must carry a @status attribute
  • Thinks a passing case must have no children whatsoever
  • Assumes a case that never ran looks different from one that passed
open as a page

In the JUnit-XML result file Maven Surefire writes, a re-run test case can appear with `<flakyFailure>` children, or with a `<failure>` followed by `<rerunFailure>` children. What does each of those two shapes say about how the case ended?

level: juniorimportance: must knowfreq 44%

basics

~10 s

<flakyFailure> records a failed attempt of a case that eventually passed, so the build stays green. A <failure> plus <rerunFailure> records a case that failed on every attempt, so the build goes red.

open as a page

In a JUnit-XML result file written to the Maven Surefire report schema, which attributes does a `<testcase>` element require, which are optional, and what does a reader have to work with when it needs to tell two cases apart?

level: juniorimportance: must knowfreq 56%

basics

~20 s

A <testcase> requires only @name and @time; @classname, @group and @timestamp are optional. The identity the file declares is therefore @classname plus @name - and a writer may legally omit the @classname half of that pair.

open as a page

JUnit 5's `LegacyXmlReportGeneratingListener` writes a `@hostname` attribute on `<testsuite>` that the Maven Surefire report schema never declares. Why does that not break the tools reading the file, and what else differs between the two writers?

level: middleimportance: must knowfreq 52%

basics

~20 s

Nothing validates these files. JUnit 5's legacy writer emits hostname unconditionally, falling back to the literal <unknown host>; the Surefire schema declares no such attribute, so the file fails that schema and readers, which never validate, consume it anyway.

open as a page

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%

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.

open as a page

When JUnit 5's `LegacyXmlReportGeneratingListener` writes a failed test case to JUnit-XML, what decides whether it emits `<failure>` or `<error>`, and why does the choice matter to whatever reads the file?

level: middleimportance: must knowfreq 66%

basics

~20 s

The thrown object decides. An AssertionError becomes <failure>; any other throwable becomes <error>. The split matters because consumers bucket them differently -- Allure 2's JUnit-XML reader records FAILED for one and BROKEN for the other.

open as a page

A parser reads the stack trace out of `<failure>` in a Maven Surefire JUnit-XML file by taking the element's text, then gets an empty string when it applies the same code to `<rerunFailure>`. What is different about how those two elements carry a trace?

level: middleimportance: must knowfreq 33%

basics

~10 s

<failure> and <error> are simple content: the trace is the element's own text. The four repetition elements are complex, and each requires a <stackTrace> child exactly once, so their trace sits one level deeper.

open as a page

In the Maven Surefire report schema (`surefire-test-report.xsd`), which attributes must every `<testsuite>` element carry, and what does that schema actually guarantee about how those counters relate to the `<testcase>` elements inside it?

level: middleimportance: must knowfreq 62%

basics

~20 s

Every <testsuite> must carry @name, @tests, @errors, @skipped and @failures; @time and @timestamp are optional. The schema only requires those attributes to be present - it never makes their values agree with the <testcase> elements below.

open as a page

The legacy JUnit-XML shape has no field for a JUnit 5 test's unique id or its full display name. Where does `LegacyXmlReportGeneratingListener` put them, and what does `OpenTestReportGeneratingListener` do instead?

level: middleimportance: should knowfreq 42%

basics

~10 s

The legacy XML has no field for either, so JUnit 5's listener writes unique-id: and display-name: lines into the free-text system-out element. Open Test Reporting gives them real elements instead: uniqueId, legacyReportingName and type.

open as a page

In the Maven Surefire report schema, what does the `<properties>` block inside a `<testsuite>` hold, and why can a fact about one individual `<testcase>` never be recorded there?

level: middleimportance: should knowfreq 38%

basics

~20 s

<properties> holds a flat list of <property> elements, each carrying a @name and a @value attribute. It is a child of <testsuite>, so everything in it describes the whole file - <testcase> has no properties block of its own.

open as a page

A dashboard shows a red case from a JUnit-XML file with an empty failure message although the stack trace is there. In `<failure>` and `<error>`, where do the message, the exception type and the trace each live, and what makes the message go missing?

level: seniorimportance: should knowfreq 54%

basics

~20 s

Both elements are simple content: the stack trace is the element's own text, while the message and exception type ride on the optional @message and @type attributes. A throwable whose message is null means no @message is written at all.

open as a page

`<system-out>` and `<system-err>` are the only free-text channels the JUnit-XML result format offers a test case. What can they actually carry, and what should you not expect a consumer to do with them?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Each is a plain string element, at most one per <testcase> in the Surefire schema, holding captured output as text -- no media type, no filename, no bytes. And a consumer may read one channel and ignore the other entirely.

open as a page

Allure 2's JUnit-XML reader decides a case is flaky by checking whether the `<testcase>` contains `<rerunError>` or `<rerunFailure>`, and never looks at `<flakyFailure>` or `<flakyError>`. What does that produce in the generated report, and what does it tell you about relying on this file?

level: seniorimportance: should knowfreq 27%

basics

~10 s

The report inverts the file's meaning. Cases labelled flaky are the ones Maven Surefire recorded as failing on every attempt, while the cases Surefire actually calls flaky get no mark at all.

open as a page

One `<testcase>` element in a Maven Surefire JUnit-XML file carries a `<failure>` and two `<rerunFailure>` children. How many times did that case actually run, and why is that count not a field in the file?

level: seniorimportance: should knowfreq 29%

basics

~20 s

Three times: one <failure> plus two <rerunFailure> children is three failed attempts. No field records it - the file keeps one <testcase> per case, so the attempt count is arithmetic over that element's outcome children.

open as a page

Maven Surefire writes one JUnit-XML file per test class, named `TEST-<sourceName>.xml`, and each file's `<testsuite>` carries its own `@tests`, `@failures`, `@errors` and `@skipped`. Where does a run-level total come from, and what happens to it when one of those files is never written?

level: seniorimportance: should knowfreq 47%

basics

~20 s

No file holds a run total. Each TEST-<sourceName>.xml declares counters for its own suite only, so the run figure is a consumer's sum over the glob - and a file that was never written drops out of that sum silently.

open as a page

Your build's JUnit-XML result files are read by several tools and none of them validates against `surefire-test-report.xsd`. How do you decide which dialect to emit and what to treat as the contract?

level: principalimportance: should knowfreq 34%

basics

~20 s

The contract is what your consumers actually read, not the one published schema. Emit the shape every reader handles and pin it with a checked-in sample. Add a second format only when the legacy one cannot carry what you need.

open as a page

In the Maven Surefire report schema, `<failure>` may appear any number of times inside one `<testcase>` while `<error>` and `<skipped>` may appear at most once. What does that let a file express, and where does it break down?

level: middleimportance: nice to knowfreq 42%

basics

~20 s

<failure> is declared unbounded while <error> and <skipped> are capped at one, so a case may carry several assertion failures but only a single error or skip. A writer emitting two <error> elements produces a file the schema rejects.

open as a page

A build with no re-runs configured still writes `flakes="0"` on every `<testsuite>` in the JUnit-XML Maven Surefire produces. What can a reader conclude from that attribute being present, and what only from its value?

level: middleimportance: nice to knowfreq 22%

basics

~10 s

Presence says almost nothing: Maven Surefire writes @flakes on every <testsuite> unconditionally, using 0 when nothing flaked. Only a non-zero value reports anything - the count of cases that failed and then passed.

open as a page

The Maven Surefire report schema declares `@time` and `@timestamp` on `<testsuite>` as optional, while `@time` on `<testcase>` is required. What may a consumer assume about a suite's duration and start time, and how should it behave when they are absent?

level: middleimportance: nice to knowfreq 29%

basics

~20 s

Nothing is guaranteed at suite level: @time and @timestamp are optional, so a valid file may carry neither. Only <testcase> @time is required, so a reader can sum case durations - which is a different quantity.

open as a page

A stack trace contains a control character that XML 1.0 forbids. Maven Surefire and JUnit 5's `LegacyXmlReportGeneratingListener` answer that differently — what does each do, and what does the difference cost a tool that reads both?

level: seniorimportance: nice to knowfreq 32%

basics

~20 s

The two writers disagree. Surefire preserves the character by escaping it twice, so a reader that knows the convention can recover it; JUnit 5 substitutes the replacement character U+FFFD and splits a section-closing sequence across two CDATA sections.

open as a page