A Java codebase has several thousand JUnit 5 tests whose names appear as terse, unreadable identifiers in CI reports. How would you get consistent, human-readable names across the suite without editing every test method, and what would you watch out for?
answer
- junit.jupiter.displayname.generator.default in junit-platform.properties
- Precedence: @DisplayName > class generator > configured default > Standard
- Custom generator extends Simple, needs a no-arg constructor
- Labels ≠ unique IDs → selection safe, label-keyed history churns
- Generator gives consistency, not meaning — pair with a naming convention
basics
~20 sSet junit.jupiter.displayname.generator.default in a junit-platform.properties file on the test classpath to a generator (ReplaceUnderscores, or a custom camelCase splitter). Existing @DisplayName annotations still win, so nothing is clobbered. Watch out: labels are not identities, so tooling keyed on displayed names will churn.
solid answer
~50 sTurn on a **suite-wide display-name generator** rather than annotating thousands of methods. Put a `junit-platform.properties` on the test classpath with: ``` junit.jupiter.displayname.generator.default=com.acme.test.CamelCaseGenerator ``` A custom generator extending `DisplayNameGenerator.Simple` that splits camelCase gets readable labels out of existing method names with **zero source changes**; `ReplaceUnderscores` is the built-in choice if the codebase already uses `snake_case`. Why it is safe: precedence runs explicit `@DisplayName` → class-level `@DisplayNameGeneration` → the configured default → `Standard`. Hand-written labels and per-class opt-outs survive. What to watch: - Display names are **not identities** — unique IDs still use class/method names — so test selection is unaffected, but any dashboard grouping history by *label* will see every test look "new". - `IndicativeSentences` produces very long labels once `@Nested` chains are deep. - Non-ASCII in labels can upset downstream report consumers. - A generator is cosmetic: it makes bad method names *pretty*, not *meaningful*. Pair it with a naming convention enforced in review.
code
java · 19 lines// src/test/resources/junit-platform.properties
// junit.jupiter.displayname.generator.default=com.acme.test.CamelCaseGenerator
package com.acme.test;
import org.junit.jupiter.api.DisplayNameGenerator;
import java.lang.reflect.Method;
public class CamelCaseGenerator extends DisplayNameGenerator.Simple {
public CamelCaseGenerator() { } // no-arg constructor is required
@Override
public String generateDisplayNameForMethod(Class<?> testClass, Method method) {
return method.getName()
.replaceAll("(?<=[a-z0-9])(?=[A-Z])", " ")
.replace('_', ' ');
}
}go deeper
Recall that a generator can be configured once instead of annotating each method, and that ReplaceUnderscores is the common built-in.
Name the configuration parameter and the properties file, state the precedence chain, and explain why existing @DisplayName annotations survive.
Lead with the tradeoff and the rollout plan, and call out the real risks: label-keyed reporting history churning, over-long IndicativeSentences labels, non-ASCII in reports, and cosmetics not substituting for a naming convention.
Treat it as a suite-wide convention decision — one generator or per-module opt-in, how the convention is enforced in review, and what downstream reporting and flaky-test tooling depends on test labels versus unique IDs.
## Framing the problem The complaint is about the **report**, not the code. Two levers exist: annotate every method with `@DisplayName` (thousands of edits, and permanently two names per test to keep in sync), or configure a **display-name generator** that derives labels from the method names you already have. At this scale, only the second is defensible. ## The mechanism JUnit Jupiter resolves a test's label in this order: 1. An explicit `@DisplayName` on the method or class. 2. A `@DisplayNameGeneration(...)` declared on the class or inherited from an enclosing/parent class. 3. The configuration parameter `junit.jupiter.displayname.generator.default`. 4. `DisplayNameGenerator.Standard` (method name plus parentheses). Step 3 is the lever. Set it to the fully-qualified name of a `DisplayNameGenerator` implementation in a `junit-platform.properties` file on the test classpath and it applies to every Jupiter test in the suite: ``` junit.jupiter.displayname.generator.default=\ org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores ``` (The `$` matters: `ReplaceUnderscores` is a nested class.) ## Choosing the generator - If methods are `snake_case`, `ReplaceUnderscores` is a one-line fix. - If they are camelCase — the usual case in a large Java codebase — write a small generator that extends `DisplayNameGenerator.Simple` and splits on the lower-to-upper boundary. It needs a public no-arg constructor because the engine instantiates it reflectively, and it should live in the test sources of a shared module so every module picks it up. - `IndicativeSentences` is attractive for behaviour-style suites organised with `@Nested`, but it composes the whole enclosing chain into one label, so a three-level nesting yields a very long line. Prefer to opt individual suites into it with a class-level `@DisplayNameGeneration` rather than making it the global default. ## Why the rollout is non-destructive Because the configured default sits **below** both explicit annotations and class-level generators in the precedence chain: - every hand-written `@DisplayName` keeps its text; - any class that already declares its own generator keeps its behaviour; - a team that dislikes the global choice can override it for their package by putting `@DisplayNameGeneration` on a shared base class. That means the change can go in as a single commit and be reverted as a single commit. ## The caveats worth naming **Labels are not identities.** A test's unique ID is derived from the engine, class and method names; display names take no part. So: - name-based test selection, re-run-failed, and IDE navigation are unaffected — nothing breaks; - but any reporting system, flaky-test tracker or history dashboard that keys on the *displayed* name will treat every test as newly appeared on the first build after the change, losing continuity. If such a system exists, land the change deliberately and warn its owners, or verify it keys on unique IDs. **Cosmetics are not clarity.** A generator turns `testCase3` into `test case 3` — still meaningless. The generator buys consistency and removes ceremony; it does not fix names that never described a behaviour. The durable fix pairs it with a review-enforced convention (subject, condition, expected outcome) applied to new and touched tests, so quality improves as the code is worked on rather than in one heroic rename. **Report encoding.** Free-form labels can carry non-ASCII characters. That is legal for JUnit but some XML report consumers and dashboards mangle or reject them, so a global generator should produce plain ASCII. **Parameterized and repeated tests are separate.** Their per-invocation labels come from the `name` attribute template on `@ParameterizedTest` / `@RepeatedTest`, which composes around the method's display name via the `{displayName}` placeholder. A generator improves the base label but does not fix an unhelpful invocation template — those need their own pass if the arguments are not showing up usefully. **Verify the properties file actually loads.** The configuration parameter takes effect only if `junit-platform.properties` is on the **test** classpath at execution time. A quick sanity check is one test asserting on `TestInfo.getDisplayName()`, or simply eyeballing the first CI report after the change — a silent no-op here is the most common way this rollout "doesn't work". ## Sequencing the change 1. Write or pick the generator; unit-test it against a handful of representative method names. 2. Enable it in one module, inspect the report, and confirm nothing downstream broke. 3. Roll out to the rest via the shared test classpath. 4. Adopt the naming convention in review, and reserve explicit `@DisplayName` for the cases where a sentence genuinely beats an identifier — business rules, `@Nested` context labels, characters an identifier cannot hold.
- Does switching on a suite-wide display-name generator risk breaking anything that runs or selects tests by name?No. JUnit derives unique IDs from the engine, class and method names, and display names take no part in them, so filters, re-run-failed and IDE navigation are untouched. The real exposure is downstream: a reporting or flaky-test system that groups historical results by the displayed label will see every test as brand new on the first build after the change, so its owners should be warned or the tool confirmed to key on unique IDs.
- Why not simply annotate every test with @DisplayName instead?It is thousands of edits that create a permanent duplicate — the method name and the label — and in practice one of them drifts out of date whenever the test changes. A generator gives one source of truth and applies uniformly to new tests without anyone remembering an annotation. Explicit @DisplayName then earns its place only where a sentence beats an identifier: business rules, @Nested context labels, or text needing characters a Java identifier cannot hold.
saying these in an interview costs you the question
- Proposing a mass edit to add @DisplayName to thousands of methods as the primary solution.
- Fearing that renaming display names will break test selection or re-runs.
- Expecting a generator to override existing explicit @DisplayName annotations.
- Presenting cosmetic renaming as a fix for method names that never described a behaviour.
- Forgetting that the configuration parameter only takes effect if junit-platform.properties is actually on the test classpath.