Skip to main content

On This Page

A Guide to @ClassTemplate in JUnit 5

2 min read
Share

These articles are AI-generated summaries. Please check the original sources for full details.

1. Introduction

JUnit 5’s @ClassTemplate annotation allows a single test class to be executed multiple times, each with a different configuration, simplifying testing across various environments. This feature addresses the need for versatile testing, particularly when dealing with environment-dependent behavior.

Why This Matters

Traditional testing often requires duplicating test classes or embedding complex conditional logic to accommodate different environments. This approach leads to maintenance overhead and potential inconsistencies. The @ClassTemplate annotation provides a cleaner solution by separating test logic from environmental configuration, reducing code duplication and improving test reliability, especially in scenarios involving localization or feature flagging where environment-specific behavior is critical.

Key Insights

  • InvocationContextProviders: Introduced in JUnit 5 for managing test execution contexts.
  • Provider-Driven Execution: JUnit relies on providers to define how class templates are executed, promoting separation of concerns.
  • Locale Extension Example: Demonstrates a practical application of @ClassTemplate for testing environment-specific behavior, such as date formatting across different locales.

Working Example

class DateFormatter {
    public String format(LocalDate date) {
        DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)
                .withLocale(Locale.getDefault());
        return date.format(formatter);
    }
}
class LocaleExtension implements BeforeEachCallback, AfterEachCallback {
    private final Locale locale;
    private Locale previous;

    @Override
    public void beforeEach(ExtensionContext context) {
        previous = Locale.getDefault();
        Locale.setDefault(locale);
    }

    @Override
    public void afterEach(ExtensionContext context) {
        Locale.setDefault(previous);
    }
}
class DateLocaleClassTemplateProvider implements ClassTemplateInvocationContextProvider {
    @Override
    public Stream<ClassTemplateInvocationContext> provideClassTemplateInvocationContexts(ExtensionContext context) {
        return Stream.of(Locale.US, Locale.GERMANY, Locale.ITALY, Locale.JAPAN)
                .map(this::invocationContext);
    }

    private ClassTemplateInvocationContext invocationContext(Locale locale) {
        return new ClassTemplateInvocationContext() {
            @Override
            public String getDisplayName(int invocationIndex) {
                return "Locale: " + locale.getDisplayName();
            }

            @Override
            public List<Extension> getAdditionalExtensions() {
                return List.of(new LocaleExtension(locale));
            }
        };
    }
}
private final DateFormatter formatter = new DateFormatter();
@Test
void givenDefaultLocale_whenFormattingDate_thenMatchesLocalizedOutput() {
    LocalDate date = LocalDate.of(2025, 9, 30);
    DateTimeFormatter expectedFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)
            .withLocale(Locale.getDefault());
    String expected = date.format(expectedFormatter);
    String formatted = formatter.format(date);
    LOG.info("Locale: {}, Expected: {}, Formatted: {}", Locale.getDefault(), expected, formatted);
    assertEquals(expected, formatted);
}

Practical Applications

  • Localization Testing: Verifying application behavior across different locales, as demonstrated in the example.
  • Feature Flag Testing: Running tests with different feature flags enabled or disabled to ensure proper functionality under various configurations.

References:

Continue reading

Next article

Building a Modular Starter Kit for M5StickC-Plus2: From Messy Code to Clean Architecture

Related Content