skip to content

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%

answer

  1. the format has no slot for identity
  2. free text is the only channel left
  3. unique-id and display-name lines
  4. the newer format gives them real elements

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.

solid answer

~40 s

The legacy shape can carry a `@name` and a `@classname` on `<testcase>` and nothing else about identity, so `LegacyXmlReportGeneratingListener` smuggles the rest into free text: it writes a `unique-id:` line and a `display-name:` line into a `<system-out>` element, for the suite and again for each case. The `@name` it writes is the platform's legacy reporting name, not the display name a reader sees in an IDE. Anything that wants the real id has to parse prose out of an output stream. `OpenTestReportGeneratingListener` is the answer to that: it writes a different document in the `https://schemas.junit.org/open-test-reporting` namespace, where a node's metadata carries `uniqueId`, `legacyReportingName` and `type` as **elements** — `<junit:type>` holds `TEST`, `CONTAINER` or `CONTAINER_AND_TEST`, backed one-to-one by the platform's own descriptor type.

code

xml · 12 lines
xml
<?xml version="1.0" encoding="UTF-8"?>
<e:events xmlns:e="https://schemas.opentest4j.org/reporting/events/0.2.0"
          xmlns="https://schemas.opentest4j.org/reporting/core/0.2.0"
          xmlns:junit="https://schemas.junit.org/open-test-reporting">
  <e:started id="2" name="CartTest" parentId="1" time="2026-03-06T12:56:57.153Z">
    <metadata>
      <junit:uniqueId>[engine:junit-jupiter]/[class:com.example.CartTest]</junit:uniqueId>
      <junit:legacyReportingName>com.example.CartTest</junit:legacyReportingName>
      <junit:type>CONTAINER</junit:type>
    </metadata>
  </e:started>
</e:events>

go deeper

for a junior

Know that a JUnit 5 test's real identifier does not fit anywhere in the old XML shape, and that the writer therefore prints it as text inside the captured-output element.

for a middle

Explain both answers: the legacy listener smuggles unique-id and display-name lines into free text, while the open format carries uniqueId, legacyReportingName and type as elements in their own namespace.

for a senior

Show what the smuggle costs a consumer in practice — parsing prose out of a stream that also holds whatever the test printed — and be able to say which artefact you would actually build an ingester against.

for a principal

Own the migration question: running a second reporting format alongside the old one indefinitely, and deciding what has to be true of your consumers before the legacy artefact can stop being produced.

## What the legacy shape has no room for A JUnit 5 test has an identity the platform treats as canonical: a **unique id** that names the engine, the container chain and the test itself, and that survives renaming a display label. The legacy XML shape has nowhere to put it. Its `<testcase>` carries `@name`, `@classname` and `@time`, and its `<testsuite>` carries a name, some counts and a few timestamps. There is no id attribute, no field for a node's type, and no way to say that one node contains another beyond the single suite-to-case nesting the format already has. That is not an oversight anyone can fix. The shape predates every identity model in modern JUnit, and there is no owner to extend it — adding an attribute is exactly the dialect drift that makes files stop validating and readers stop agreeing. ## Where JUnit 5 puts identity anyway `LegacyXmlReportGeneratingListener` takes the only channel the format leaves open: **free text**. It builds a two-line block and writes it into a `<system-out>` element: - a `unique-id:` line carrying the platform's unique id for the node, and - a `display-name:` line carrying the node's display names from the root down, joined into one readable path. That block is written for the suite, and again as the first output element on every `<testcase>`. The `@name` attribute on `<testcase>`, meanwhile, is the platform's **legacy reporting name** — the name chosen to look right in a format that expects a method name, which is not necessarily the display name a reader sees in an IDE. So the same case carries two different names in the same document, in two different channels, and only one of them is where a naive reader looks. The cost of the smuggle is easy to state: 1. **It is prose, not structure.** A consumer wanting the id must string-match inside an output stream that also carries whatever the test itself printed. 2. **It shares a channel.** Test output and injected metadata live in the same element, so a test that prints a line beginning `unique-id:` is indistinguishable from the writer's own. 3. **Nothing declares it.** No schema mentions those lines, so no reader can be expected to know they are there. ## The replacement format `OpenTestReportGeneratingListener` writes an entirely different document instead of stretching the old one. It is an event stream in the opentest4j reporting namespaces, and JUnit's own additions live in the namespace `https://schemas.junit.org/open-test-reporting`, which contributes exactly three elements: | element | what it carries | |---|---| | `uniqueId` | the platform's unique id for the node | | `legacyReportingName` | the name the legacy XML shape would have used | | `type` | `TEST`, `CONTAINER` or `CONTAINER_AND_TEST` | Two things about that table are worth saying out loud. First, `type` is an **element**, not an attribute: it is written `<junit:type>CONTAINER</junit:type>` inside a metadata block, not `type="CONTAINER"` on the node. Writing it as an attribute is the single commonest mistake in code that generates or matches these documents. Second, `CONTAINER_AND_TEST` exists because the platform's descriptor type has a value for a node that both holds children and executes as a test itself — a shape the legacy format cannot express at all, since a `<testcase>` there can never contain another `<testcase>`. Keeping `legacyReportingName` beside `uniqueId` is a deliberate bridge: a consumer that only knows the old vocabulary can still find the name it expects, while a consumer that wants real identity has it as structure rather than as prose. ## Turning either one on This is where a lot of time gets wasted, so it is worth being blunt: **there is no configuration parameter that enables the legacy XML report.** The listener is registered programmatically, or activated by the Console Launcher's `--reports-dir` option, and that is the whole story. Searching for an `enabled` flag for it is a dead end. The reporting configuration parameters that do exist cover the output directory and the newer format — `junit.platform.reporting.output.dir`, `junit.platform.reporting.open.xml.enabled`, `junit.platform.reporting.open.xml.git.enabled` and `junit.platform.reporting.open.xml.socket`. Note the asymmetry: the new format has a switch, the legacy one has none. ## What to take from the pair The legacy listener and the open format are the same team's two answers to one limit, a decade apart. The first bends the format until identity fits somewhere; the second admits the format cannot hold it and writes a second document beside it. When you are deciding how to get a value out of a run and the interchange shape has no field for it, those are genuinely the only two moves available — smuggle it into free text and own the parsing, or emit a second artefact and own the migration.

  • In Open Test Reporting, is `type` an attribute on the node or an element of its own?
    An element. It is written `<junit:type>CONTAINER</junit:type>` inside the metadata block, beside `<junit:uniqueId>` and `<junit:legacyReportingName>`. Its values are `TEST`, `CONTAINER` and `CONTAINER_AND_TEST`, mapped one-to-one onto the platform's descriptor type, so a node that both holds children and runs itself is representable.
  • Which configuration parameter switches the legacy XML report on?
    None exists, in any version. The reporting parameters cover the output directory and the open XML output; the legacy listener is registered programmatically or activated by the Console Launcher's `--reports-dir` option. Hunting for an `enabled` flag for it is a common and complete dead end.

saying these in an interview costs you the question

  • Thinks the legacy XML has a field for a test id
  • Expects a config parameter to enable the legacy XML report
  • Treats the type value as an attribute rather than an element
  • Reads the testcase name attribute as the full display name