Core Java

An Introduction to @ClassTemplate in JUnit 5

JUnit 5 offers several extension points that allow developers to customize how tests are discovered and executed. One of these features is @ClassTemplate. While many developers are familiar with parameterized tests and method-level templates, @ClassTemplate works at a higher level by allowing the same test class to run multiple times, each with a different execution context.

This makes @ClassTemplate particularly useful for verifying behaviour across different environments, configurations, or infrastructure setups without the need to duplicate entire test classes. In this article, we will explore what @ClassTemplate is, when it should be used, and how to implement it in practice.

1. What Is @ClassTemplate?

@ClassTemplate marks a test class as a template that can be executed multiple times. Instead of executing the class once, the JUnit engine requests one or more invocation contexts from a ClassTemplateInvocationContextProvider. Each invocation context represents a full execution of the test class and can contribute extensions, configuration, and custom display names.

This means the same set of test methods can be executed repeatedly under different conditions, with each run appearing separately in test reports.

When Should You Use @ClassTemplate?

@ClassTemplate is most useful when differences between test runs affect the execution context rather than just the input values. Examples include switching between implementations, changing configuration modes, toggling runtime features, or validating behavior across environments.

If only method inputs change, parameterized tests are often simpler. However, when the whole class needs to be executed under distinct setups, class templates provide a clearer structure and better isolation.

2. Prerequisite

To follow along with this guide, you should be using Java 17 or newer, as JUnit 5 now aligns with modern Java baselines and recent tooling improvements. It is also important to note that the @ClassTemplate annotation is a recent addition to JUnit 5, introduced in JUnit 5.13.x release line. As a result, older versions of the JUnit platform and build plugins may not recognise or execute class templates correctly.

To avoid compatibility issues, ensure you are using the latest versions of JUnit Jupiter, the JUnit Platform Launcher, and the Maven Surefire Plugin. The following pom.xml snippet shows a minimal configuration that supports @ClassTemplate:

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.14.1</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-launcher</artifactId>
            <version>1.14.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.5.3</version>
            </plugin>
        </plugins>
    </build>

3. Example Scenario: Testing with Different Discount Rates

In this example, we want to test a pricing service that applies a discount to a product price. The same test logic should run with two configurations: one with no discount and another with a seasonal discount. Instead of writing separate test classes or adding conditional logic inside the test, we use @ClassTemplate to run the same class twice with different parameters.

This keeps the test logic simple while still validating behavior under multiple configurations.

Service Under Test

The service calculates a final price based on a configured discount rate.

public class PricingService {

    private final double discountRate;

    public PricingService(double discountRate) {
        this.discountRate = discountRate;
    }

    public double calculateTotal(double price) {
        return price - (price * discountRate);
    }
}

The discount rate is supplied through the constructor, making it easy to test different configurations without modifying the class itself.

4. Class Template Invocation Context Provider

The invocation context provider defines how many times the test class runs and what configuration is applied to each execution.

public class DiscountClassTemplateProvider implements ClassTemplateInvocationContextProvider {

    @Override
    public boolean supportsClassTemplate(ExtensionContext context) {
        return context.getTestClass()
                .map(c -> c.isAnnotationPresent(ClassTemplate.class))
                .orElse(false);
    }

    @Override
    public Stream<ClassTemplateInvocationContext>
            provideClassTemplateInvocationContexts(ExtensionContext context) {

        return Stream.of(
                contextFor("No Discount", 0.0, 100.0),
                contextFor("Seasonal Discount", 0.20, 80.0)
        );
    }

    private ClassTemplateInvocationContext contextFor(String name, double rate, double expected) {

        ParameterResolver resolver = new ParameterResolver() {

            @Override
            public boolean supportsParameter(ParameterContext pc, ExtensionContext ec) {

                Class<?> type = pc.getParameter().getType();
                return type == double.class || type == Double.class;
            }

            @Override
            public Object resolveParameter(ParameterContext pc, ExtensionContext ec) {

                if (pc.getIndex() == 0) {
                    return rate;
                }
                return expected;
            }
        };

        return new ClassTemplateInvocationContext() {

            @Override
            public String getDisplayName(int invocationIndex) {
                return name;
            }

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

This provider supplies two invocation contexts, each with a descriptive display name. For each run, it registers a ParameterResolver that injects method parameters into the test. The first parameter receives the discount rate, and the second receives the expected total price. This approach avoids modifying the test instance directly and keeps all configuration scoped to the specific class execution.

5. Test Class Using @ClassTemplate

The test class defines a field that will be initialized differently for each class invocation.

@ClassTemplate
@ExtendWith(DiscountClassTemplateProvider.class)
class PricingServiceClassTemplateTest {

    private static final Logger LOGGER = Logger.getLogger(PricingServiceClassTemplateTest.class.getName());

    @Test
    void givenPrice_whenCalculatingTotal_thenDiscountIsApplied(
            double discountRate, double expectedTotal) {

        LOGGER.info(() -> "Running with discountRate=" + discountRate + ", expectedTotal=" + expectedTotal);

        PricingService service = new PricingService(discountRate);

        double total = service.calculateTotal(100.0);

        assertEquals(expectedTotal, total);
    }
}

This test class is annotated with @ClassTemplate, which tells JUnit to execute the entire class multiple times using different invocation contexts provided by the registered extension. The @ExtendWith(DiscountClassTemplateProvider.class) annotation connects the test class to the class template provider that supplies the different discount configurations.

The test method receives its parameters directly from the class template invocation context. The same test logic runs for every class execution, but with different injected values. This keeps the test concise and avoids any conditional branching inside the test itself.

Running the Tests

Once the setup is complete, the tests can be run either from an IDE or from the command line using mvn test, and because the test class is marked as a class template, JUnit executes the entire class separately for each invocation context supplied by the DiscountClassTemplateProvider, effectively running the same test logic multiple times under different configurations.

Sample Output

[INFO] Running com.jcg.example.PricingServiceClassTemplateTest
[INFO] Running com.jcg.example.PricingServiceClassTemplateTest
Running with discountRate=0.0, expectedTotal=100.0
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.313 s -- in com.jcg.example.PricingServiceClassTemplateTest
[INFO] Running com.jcg.example.PricingServiceClassTemplateTest
Running with discountRate=0.2, expectedTotal=80.0
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.281 s -- in com.jcg.example.PricingServiceClassTemplateTest
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.084 s -- in com.jcg.example.PricingServiceClassTemplateTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

The output shows the same test class executed twice, once for each configuration. Each execution is labelled, making it easy to see which configuration is being tested. Even though there is only one test method, the total number of executed tests is two because the entire class is executed per configuration.

If one configuration were to fail, the report would clearly indicate which class execution failed, making diagnosis straightforward.

5. Conclusion

In this article, we explored how @ClassTemplate in JUnit 5 enables class-level test reuse by executing the same test class multiple times with different execution contexts, making it a powerful tool for validating behavior across varying configurations. This approach keeps tests clean, avoids duplication, and provides clear isolation between configurations, making it useful for scenarios such as feature variations, environment differences, or multiple implementations that must all satisfy the same contract.

6. Download the Source Code

This article provided a guide to using ClassTemplate in JUnit5.

Download
You can download the full source code of this example here: junit5 classtemplate guide

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

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

0 Comments
Oldest
Newest Most Voted
Back to top button