JUnit 6 Test Naming Conventions for Readable and Maintainable Tests

Good test names are one of the highest-value investments you can make in a test suite. A test named test1() tells you nothing when it fails at 2am. A test named shouldThrowIllegalArgumentExceptionWhenEmailIsNull tells you exactly what broke, why it matters, and where to look. This guide covers every JUnit 6 test naming convention and strategy β€” from method names and @DisplayName to display name generators and custom naming strategies.

Why Test Naming Matters

Test names serve as living documentation. They communicate:

  • What is being tested (the method or feature)
  • When β€” under what conditions
  • What is expected to happen

A well-named test suite is searchable, self-documenting, and produces build reports that any developer β€” or product owner β€” can read and understand without opening the source code.

Strategy 1: The Should-When Pattern

The most widely adopted Java test naming convention. The method name reads as a sentence: "should [expected behaviour] when [condition]."

class PasswordValidatorTest {

    // Pattern: should[ExpectedBehaviour]When[Condition]
    @Test
    void shouldReturnTrueWhenPasswordMeetsAllRequirements() { }

    @Test
    void shouldReturnFalseWhenPasswordIsShorterThanEightCharacters() { }

    @Test
    void shouldThrowIllegalArgumentExceptionWhenPasswordIsNull() { }

    @Test
    void shouldRequireAtLeastOneUppercaseLetter() { }

    @Test
    void shouldRequireAtLeastOneSpecialCharacter() { }
}

Strategy 2: The Given-When-Then Pattern (BDD Style)

Derived from Behaviour-Driven Development (BDD). Works well in teams that also write Gherkin-style specifications:

class ShoppingCartTest {

    // Pattern: given[Context]_when[Action]_then[ExpectedOutcome]
    // Underscores are allowed in test method names β€” JUnit 6 accepts them
    @Test
    void givenEmptyCart_whenAddingFirstItem_thenCartContainsOneItem() { }

    @Test
    void givenCartWithItems_whenApplyingValidCoupon_thenTotalIsReduced() { }

    @Test
    void givenCartWithItems_whenApplyingExpiredCoupon_thenExceptionIsThrown() { }

    @Test
    void givenCartAtMaxCapacity_whenAddingAnotherItem_thenCartFullExceptionIsThrown() { }
}

Strategy 3: @DisplayName β€” Full English Sentences

@DisplayName decouples the method name from the report display name. Use it to write complete, grammatically correct sentences that appear in IDE and HTML reports:

@DisplayName("PasswordValidator β€” password strength and validation rules")
class PasswordValidatorTest {

    // Method name = concise identifier for developers
    // @DisplayName = full readable description for everyone
    @Test
    @DisplayName("A password with 8+ chars, uppercase, lowercase, digit, and symbol is valid")
    void strongPasswordIsValid() { }

    @Test
    @DisplayName("A password shorter than 8 characters is rejected")
    void shortPasswordIsRejected() { }

    @Test
    @DisplayName("A null password throws IllegalArgumentException with descriptive message")
    void nullPasswordThrowsIllegalArgumentException() { }

    @Test
    @DisplayName("Unicode characters in passwords are handled correctly")
    void unicodePasswordIsHandledCorrectly() { }
}

IntelliJ IDEA test runner output with @DisplayName:

PasswordValidator β€” password strength and validation rules
  βœ” A password with 8+ chars, uppercase, lowercase, digit, and symbol is valid
  βœ” A password shorter than 8 characters is rejected
  βœ” A null password throws IllegalArgumentException with descriptive message
  βœ” Unicode characters in passwords are handled correctly

Tests run: 4, Failures: 0

Strategy 4: @DisplayNameGeneration β€” Automatic Name Formatting

JUnit 6 can auto-generate display names from method names using built-in generators. This avoids the overhead of writing @DisplayName on every method while still producing readable reports:

import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;

// ReplaceUnderscores: converts method_name_like_this to "method name like this"
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class OrderServiceTest {

    // Method name:    creating_order_with_valid_data_sets_status_to_pending
    // Generated name: creating order with valid data sets status to pending
    @Test
    void creating_order_with_valid_data_sets_status_to_pending() { }

    // Method name:    cancelling_a_completed_order_throws_exception
    // Generated name: cancelling a completed order throws exception
    @Test
    void cancelling_a_completed_order_throws_exception() { }
}

// IndicativeSentences: class name + method name as a sentence
@DisplayNameGeneration(DisplayNameGenerator.IndicativeSentences.class)
class InvoiceCalculatorTest {

    // Generated name: "InvoiceCalculator: adding line items increases total"
    @Test
    void adding_line_items_increases_total() { }
}

Strategy 5: Global Name Generator via junit-platform.properties

Set the display name generator for the entire project in src/test/resources/junit-platform.properties:

# Apply ReplaceUnderscores globally to all test classes
junit.jupiter.displayname.generator.default=
    org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores

Custom DisplayNameGenerator

Build your own generator by implementing DisplayNameGenerator:

import org.junit.jupiter.api.DisplayNameGenerator;
import java.lang.reflect.Method;

/**
 * Custom generator: replaces camelCase with spaces and adds emoji prefix.
 * Example: "creatingOrderSetsPendingStatus" becomes "βœ… Creating order sets pending status"
 */
public class EmojiDisplayNameGenerator implements DisplayNameGenerator {

    @Override
    public String generateDisplayNameForClass(Class testClass) {
        // Use the simple class name with spaces added between camel-case words
        return splitCamelCase(testClass.getSimpleName().replace("Test", ""));
    }

    @Override
    public String generateDisplayNameForNestedClass(Class nestedClass) {
        return splitCamelCase(nestedClass.getSimpleName());
    }

    @Override
    public String generateDisplayNameForMethod(Class testClass, Method testMethod) {
        // Add emoji prefix and split camelCase method name into words
        return "βœ… " + splitCamelCase(testMethod.getName());
    }

    // Convert camelCase to space-separated words
    private String splitCamelCase(String name) {
        return name
            .replaceAll("([A-Z])", " $1") // insert space before each uppercase letter
            .toLowerCase()
            .trim();
    }
}

// Apply to a test class
@DisplayNameGeneration(EmojiDisplayNameGenerator.class)
class OrderServiceTest {
    @Test
    void creatingOrderSetsPendingStatus() { }
    // Generated display name: "βœ… creating order sets pending status"
}

Naming Anti-Patterns to Avoid

Anti-patternExampleProblem
Numbered teststest1(), test2()Zero information on failure
Vague verbstestOrder(), checkUser()No context about what is checked
Implementation detailstestHashMapLookupPerformance()Tests impl, not behaviour
Negative-only namestestInvalidEmail()Missing: what should happen?
AbbreviationststCrtOrdSts()Unreadable outside context
No contextworks()Meaningless on failure

Frequently Asked Questions (FAQs)

Q1: Should I use @DisplayName on every test?

Not necessarily. If your method names are already descriptive (e.g., shouldThrowWhenEmailIsNull), @DisplayName adds little value and becomes maintenance overhead. Use @DisplayName when you want sentence-quality readability in reports β€” especially for test classes that product owners or QA engineers will review. A @DisplayNameGeneration strategy applied globally is often a better balance.

Q2: Are underscores allowed in JUnit 6 test method names?

Yes. Java allows underscores in method names, and JUnit 6 has no restriction. Combined with DisplayNameGenerator.ReplaceUnderscores, methods named given_empty_cart_when_checkout_then_throw produce clean, readable display names. This is a popular style in teams that prefer BDD-style naming.

Q3: What naming convention works best for nested test classes?

Name @Nested inner classes after the scenario or state being tested, not the method. For example: WhenCartIsEmpty, WhenUserIsLoggedIn, GivenValidPaymentMethod. This creates a hierarchical test report that reads like: "ShoppingCartTest > WhenCartIsEmpty > checkout throws EmptyCartException." See JUnit 6 Nested Tests for detailed examples.

Q4: Does @DisplayName affect how tests are filtered or selected by tag?

No. @DisplayName is purely cosmetic β€” it only affects what appears in reports and IDE test panels. Test selection, filtering, and tag-based execution use the underlying method name and @Tag annotations, not the display name.

Q5: Should test class names end with “Test” or “Tests”?

By convention and Maven Surefire default configuration, both *Test and *Tests suffixes are auto-discovered. The most common convention in Java projects is the singular *Test (e.g., OrderServiceTest). Use *Tests for grouping classes that cover multiple related classes (e.g., OrderServiceTests covering several related test scenarios). Consistency within your project matters more than which suffix you choose.

See Also

Conclusion

Test naming is not a cosmetic concern β€” it is documentation. The should-when pattern, BDD-style given-when-then naming, @DisplayName sentences, and display name generators each serve different teams and contexts. Choose one strategy, apply it consistently across your project, and your test suite will serve as a readable, searchable specification of your system’s behaviour.

Next: Structuring Tests with JUnit 6 Nested Tests β€” use @Nested inner classes to group related tests and build a hierarchical, readable test plan.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.