JUnit 5 can derive readable test labels from method names automatically instead of requiring an annotation on every method. How does that mechanism work, and which strategies ship with the framework?
answer
- @DisplayNameGeneration(Generator.class) on the class; inherited by @Nested
- Standard / Simple / ReplaceUnderscores / IndicativeSentences
- IndicativeSentences separator via @IndicativeSentencesGeneration
- Custom: implement DisplayNameGenerator, no-arg constructor
- Suite-wide: junit.jupiter.displayname.generator.default (note the $ for nested classes)
basics
~20 sAnnotate the class with @DisplayNameGeneration(SomeGenerator.class). Built-ins are Standard (method name plus parentheses), Simple (drops empty parentheses), ReplaceUnderscores (underscores become spaces) and IndicativeSentences (prefixes enclosing class names). You can implement DisplayNameGenerator yourself, or set a suite-wide default via a configuration parameter.
solid answer
~50 sYou declare a generator on the class with `@DisplayNameGeneration`, and it applies to that class's methods and is inherited by its `@Nested` classes: ```java @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class DiscountTest { @Test void applies_a_discount_above_1000_eur() { } // -> "applies a discount above 1000 eur" } ``` The four built-ins: - **`Standard`** — the default: method name with `()`. - **`Simple`** — same, but drops the empty parentheses. - **`ReplaceUnderscores`** — underscores become spaces (extends `Simple`). - **`IndicativeSentences`** — joins the enclosing class display names and the method name into one sentence, separated by `, ` by default; the separator is configurable via `@IndicativeSentencesGeneration`. A custom generator implements `DisplayNameGenerator` (`generateDisplayNameForClass`, `...ForNestedClass`, `...ForMethod`) — a camelCase splitter is the usual one. For the whole suite, set the configuration parameter `junit.jupiter.displayname.generator.default` to a generator's fully-qualified class name. Precedence: explicit `@DisplayName` > class/inherited generator > configured default > `Standard`.
code
java · 21 linesimport org.junit.jupiter.api.*;
@DisplayName("Invoice totals")
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class InvoiceTotalTest {
@Test
void applies_a_discount_above_1000_eur() { }
// -> "applies a discount above 1000 eur"
@Nested
@DisplayName("when the customer is a reseller")
@IndicativeSentencesGeneration(separator = " -> ",
generator = DisplayNameGenerator.ReplaceUnderscores.class)
class Reseller {
@Test
void stacks_with_the_loyalty_discount() { }
// -> "Invoice totals -> when the customer is a reseller
// -> stacks with the loyalty discount"
}
}go deeper
Know that @DisplayNameGeneration exists and that ReplaceUnderscores turns snake_case method names into readable sentences.
List all four built-ins with their differences, note inheritance by @Nested classes, and mention the suite-wide configuration parameter.
Cover the precedence chain, writing a custom generator over Simple, and the tradeoff between generated names (one source of truth) and hand-written @DisplayName.
Decide the team convention — method-naming style, which generator, where it is configured — and account for how the chosen labels land in downstream CI reporting.
## Why generators exist Annotating every method with `@DisplayName` gives beautiful reports and two names to maintain per test. Generators invert the deal: you write one disciplined method name, and the framework derives the label. One source of truth, applied consistently, with zero per-method ceremony. ## The mechanism `DisplayNameGenerator` is an interface with three methods: ```java String generateDisplayNameForClass(Class<?> testClass); String generateDisplayNameForNestedClass(Class<?> nestedClass); String generateDisplayNameForMethod(Class<?> testClass, Method testMethod); ``` (Current 5.x also offers overloads receiving the enclosing-class chain for nested classes and methods, which is what `IndicativeSentences` uses to build its hierarchy.) You attach one with `@DisplayNameGeneration(MyGenerator.class)` on a test class. It applies to that class and is **inherited by its `@Nested` classes** and by subclasses, so a single annotation on a base class can cover a module. ## The built-in strategies - **`DisplayNameGenerator.Standard`** — the framework default. A method `appliesDiscount` becomes `appliesDiscount()`; a class becomes its simple name. - **`DisplayNameGenerator.Simple`** — identical, except that empty parentheses are removed: `appliesDiscount`. For a method with parameters the parentheses and parameter types remain, which keeps overloads distinguishable. - **`DisplayNameGenerator.ReplaceUnderscores`** — extends `Simple` and turns every underscore into a space. Combined with the `snake_case` test-method convention, `applies_a_20_percent_discount` reads as `applies a 20 percent discount`. This is by far the most used built-in. - **`DisplayNameGenerator.IndicativeSentences`** — builds a full sentence by prefixing the display names of the enclosing classes: with a class labelled `Invoice totals` and a `@Nested` class labelled `when the customer is a reseller`, a method yields `Invoice totals, when the customer is a reseller, applies a discount`. The `, ` separator and the generator used for the individual fragments are configurable with `@IndicativeSentencesGeneration(separator = " -> ", generator = ReplaceUnderscores.class)`. ## Custom generators The common custom generator splits camelCase into words, so `appliesDiscountAboveThreshold` becomes `applies discount above threshold` without requiring the team to switch to underscores: ```java class CamelCaseGenerator extends DisplayNameGenerator.Simple { @Override public String generateDisplayNameForMethod(Class<?> testClass, Method method) { return method.getName().replaceAll("(?<=[a-z0-9])(?=[A-Z])", " ").toLowerCase(); } } ``` Extending `Simple` (or `ReplaceUnderscores`) rather than implementing the bare interface means you only override what you care about. Generators must have a no-argument constructor, since the engine instantiates them reflectively. ## Applying it suite-wide Rather than annotating every class, set the **configuration parameter** `junit.jupiter.displayname.generator.default` to a generator's fully-qualified class name — typically in a `junit-platform.properties` file on the test classpath: ``` junit.jupiter.displayname.generator.default=\ org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores ``` Note the `$` for the nested class. That single line changes the default label for every test in the suite. ## Precedence From strongest to weakest: 1. An explicit `@DisplayName` on the class or method. 2. A `@DisplayNameGeneration` on the class, or inherited from an enclosing/parent class. 3. The `junit.jupiter.displayname.generator.default` configuration parameter. 4. `DisplayNameGenerator.Standard`. This ordering is what makes adoption safe: switching on a suite-wide generator never clobbers labels somebody wrote deliberately, and a single class can opt out of the suite default by declaring its own generator. ## What it does not touch Generated names are still **display only** — unique IDs and name-based selection continue to use the actual class and method names. And test templates keep their own naming: `@ParameterizedTest(name = ...)` and `@RepeatedTest(name = ...)` build per-invocation labels around the method's generated or explicit display name, which they expose as the `{displayName}` placeholder. ## Choosing a strategy `ReplaceUnderscores` is the pragmatic default if the team will accept `snake_case` test methods; a camelCase generator is the pragmatic default if it will not. `IndicativeSentences` reads wonderfully for behaviour-style suites built from `@Nested` contexts, but produces long labels that can be awkward in flat CI report tables, so it is best chosen per suite rather than globally.
- You switch on a display-name generator for the whole suite. What happens to methods that already carry an explicit @DisplayName?They keep their explicit labels. Resolution checks for an explicit @DisplayName first, then a generator declared on or inherited by the class, then the configured suite-wide default, then the Standard generator. That ordering makes the rollout non-destructive: hand-written labels are preserved and only unannotated methods change.
- What must a custom DisplayNameGenerator provide, and how does IndicativeSentences differ from the other built-ins?A custom generator implements DisplayNameGenerator — usually by extending Simple or ReplaceUnderscores and overriding only generateDisplayNameForMethod — and must have a no-argument constructor because the engine instantiates it reflectively. IndicativeSentences is different in that it does not look at the method in isolation: it composes the enclosing class and @Nested class display names with the method name into a single sentence, using a separator you can configure with @IndicativeSentencesGeneration.
saying these in an interview costs you the question
- Believing a generator overrides an explicit @DisplayName.
- Thinking ReplaceUnderscores also splits camelCase (it only replaces underscores).
- Assuming @DisplayNameGeneration must be repeated on every @Nested class — it is inherited.
- Writing the suite-wide configuration value with a dot instead of a $ before the nested generator class name.
- Claiming generated display names change how tests are selected or identified.