How do I use test templates in JUnit?

Test templates in JUnit 5 provide a powerful way to run the same test multiple times with different contexts or invocation strategies. Unlike @ParameterizedTest (which is a specialized form of test template), @TestTemplate gives you full control over how tests are invoked by requiring you to register a custom TestTemplateInvocationContextProvider.

When to Use @TestTemplate

Use test templates when you need to:

  • Run a test with different environments (e.g., different databases, browsers, or configurations).
  • Provide custom test invocation logic beyond what @ParameterizedTest or @RepeatedTest offers.
  • Inject different parameter sets and extensions per invocation.

Basic Structure

A @TestTemplate method requires at least one TestTemplateInvocationContextProvider registered via @ExtendWith.

Step 1: Define the Test Template Method

import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(MyTestTemplateProvider.class)
class UserServiceTest {

    @TestTemplate
    void testUserCreation(String environment) {
        System.out.println("Running test in environment: " + environment);
        // Your test logic here
    }
}

Step 2: Create the Invocation Context Provider

import org.junit.jupiter.api.extension.*;
import java.util.stream.Stream;
import java.util.List;

public class MyTestTemplateProvider implements TestTemplateInvocationContextProvider {

    @Override
    public boolean supportsTestTemplate(ExtensionContext context) {
        return true;
    }

    @Override
    public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
            ExtensionContext context) {
        return Stream.of(
                invocationContext("DEV"),
                invocationContext("STAGING"),
                invocationContext("PRODUCTION")
        );
    }

    private TestTemplateInvocationContext invocationContext(String environment) {
        return new TestTemplateInvocationContext() {
            @Override
            public String getDisplayName(int invocationIndex) {
                return "Environment: " + environment;
            }

            @Override
            public List<Extension> getAdditionalExtensions() {
                return List.of(new EnvironmentParameterResolver(environment));
            }
        };
    }
}

Step 3: Provide a ParameterResolver

import org.junit.jupiter.api.extension.*;

public class EnvironmentParameterResolver implements ParameterResolver {

    private final String environment;

    public EnvironmentParameterResolver(String environment) {
        this.environment = environment;
    }

    @Override
    public boolean supportsParameter(ParameterContext parameterContext,
                                     ExtensionContext extensionContext) {
        return parameterContext.getParameter().getType() == String.class;
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext,
                                   ExtensionContext extensionContext) {
        return environment;
    }
}

Real-World Example: Testing Against Multiple Configurations

Imagine testing a service against different database configurations:

@ExtendWith(DatabaseTestTemplateProvider.class)
class RepositoryTest {

    @TestTemplate
    void shouldSaveEntity(DatabaseConfig config) {
        // Test runs once for each configuration provided
        Repository repo = new Repository(config);
        assertTrue(repo.save(new Entity("test")));
    }
}

The provider can supply DatabaseConfig objects for H2, PostgreSQL, MySQL, etc.

Key Points to Remember

Feature Description
Annotation @TestTemplate
Required Provider TestTemplateInvocationContextProvider
Registration Via @ExtendWith or ServiceLoader
Invocation Count Determined by the number of contexts returned
Parameter Injection Through ParameterResolver in each context

@TestTemplate vs Other Test Annotations

  • @Test → Runs once.
  • @RepeatedTest → Runs a fixed number of times with the same context.
  • @ParameterizedTest → Runs with different arguments (built-in template).
  • @TestTemplate → Full custom control over invocation contexts and extensions.

Best Practices

  1. Use @TestTemplate only when built-in options are insufficient@ParameterizedTest covers most cases.
  2. Give meaningful display names via getDisplayName(int invocationIndex) for clear test reports.
  3. Keep providers reusable — a good provider can be shared across many test classes.
  4. Combine with other extensions to inject mocks, configurations, or lifecycle hooks per invocation.

Test templates unlock advanced testing scenarios where you need dynamic, context-aware test invocation — perfect for integration testing across multiple environments or configurations. 🚀

Leave a Reply

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