skip to content

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?

level: middleimportance: should knowfreq 33%

answer

  1. @DisplayNameGeneration(Generator.class) on the class; inherited by @Nested
  2. Standard / Simple / ReplaceUnderscores / IndicativeSentences
  3. IndicativeSentences separator via @IndicativeSentencesGeneration
  4. Custom: implement DisplayNameGenerator, no-arg constructor
  5. Suite-wide: junit.jupiter.displayname.generator.default (note the $ for nested classes)

basics

~20 s

Annotate 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 s

You 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 lines
java
import 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

for a junior

Know that @DisplayNameGeneration exists and that ReplaceUnderscores turns snake_case method names into readable sentences.

for a middle

List all four built-ins with their differences, note inheritance by @Nested classes, and mention the suite-wide configuration parameter.

for a senior

Cover the precedence chain, writing a custom generator over Simple, and the tradeoff between generated names (one source of truth) and hand-written @DisplayName.

for a principal

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.

context