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
@ParameterizedTestor@RepeatedTestoffers. - 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
- Use
@TestTemplateonly when built-in options are insufficient —@ParameterizedTestcovers most cases. - Give meaningful display names via
getDisplayName(int invocationIndex)for clear test reports. - Keep providers reusable — a good provider can be shared across many test classes.
- 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. 🚀
