Introduction
In this chapter, we will explore JUnit test templates. Test templates are useful for creating reusable test structures that can be customized for different test scenarios. This helps in avoiding duplication and makes your test code more maintainable.
What are Test Templates?
Test templates in JUnit allow you to define a base structure for your tests that can be reused with different inputs or configurations. This is particularly useful for tests that follow a common pattern but need to be executed with varying data or under different conditions.
@TestTemplate Annotation
The @TestTemplate annotation is used to define a method as a test template. This method is not executed directly but is invoked by a TestTemplateInvocationContextProvider which provides the context for each invocation of the test template.
Example: Creating Test Templates
Let’s create an example to demonstrate how to use the @TestTemplate annotation.
Step 1: Create a Class Under Test
Calculator Class
The Calculator class will have basic arithmetic operations.
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
public int multiply(int a, int b) {
return a * b;
}
public int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("Division by zero");
}
return a / b;
}
}
Step 2: Create Test Template Invocation Context Provider
We need to create a class that implements the TestTemplateInvocationContextProvider interface. This class provides the context for each invocation of the test template.
CalculatorTestInvocationContextProvider
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
import java.util.stream.Stream;
public class CalculatorTestInvocationContextProvider implements TestTemplateInvocationContextProvider {
@Override
public boolean supportsTestTemplate(ExtensionContext context) {
return true;
}
@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) {
return Stream.of(
invocationContext("2 + 3", 2, 3, 5),
invocationContext("5 - 3", 5, 3, 2),
invocationContext("2 * 3", 2, 3, 6),
invocationContext("6 / 3", 6, 3, 2)
);
}
private TestTemplateInvocationContext invocationContext(String displayName, int a, int b, int expected) {
return new CalculatorTestInvocationContext(displayName, a, b, expected);
}
}
CalculatorTestInvocationContext
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;
import org.junit.jupiter.api.extension.TestWatcher;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
public class CalculatorTestInvocationContext implements TestTemplateInvocationContext {
private final String displayName;
private final int a;
private final int b;
private final int expected;
public CalculatorTestInvocationContext(String displayName, int a, int b, int expected) {
this.displayName = displayName;
this.a = a;
this.b = b;
this.expected = expected;
}
@Override
public String getDisplayName(int invocationIndex) {
return displayName;
}
@Override
public void beforeEach(ExtensionContext context) throws Exception {
context.getStore(ExtensionContext.Namespace.GLOBAL).put("Calculator", new Calculator());
}
@Override
public void afterEach(ExtensionContext context) throws Exception {
context.getStore(ExtensionContext.Namespace.GLOBAL).remove("Calculator", CloseableResource.class).close();
}
@Override
public void afterTestExecution(ExtensionContext context) throws Exception {
TestWatcher.super.afterTestExecution(context);
}
@Override
public void beforeTestExecution(ExtensionContext context) throws Exception {
TestWatcher.super.beforeTestExecution(context);
}
public int getA() {
return a;
}
public int getB() {
return b;
}
public int getExpected() {
return expected;
}
}
Step 3: Create a Test Class with a Test Template
CalculatorTest
The CalculatorTest class will use the test template to run tests with different inputs.
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(CalculatorTestInvocationContextProvider.class)
public class CalculatorTest {
@TestTemplate
void testTemplate(CalculatorTestInvocationContext context) {
Calculator calculator = new Calculator();
int result;
switch (context.getDisplayName()) {
case "2 + 3":
result = calculator.add(context.getA(), context.getB());
break;
case "5 - 3":
result = calculator.subtract(context.getA(), context.getB());
break;
case "2 * 3":
result = calculator.multiply(context.getA(), context.getB());
break;
case "6 / 3":
result = calculator.divide(context.getA(), context.getB());
break;
default:
throw new IllegalArgumentException("Unexpected value: " + context.getDisplayName());
}
assertEquals(context.getExpected(), result);
}
}
Important Points
- @TestTemplate: Use this annotation to mark a method as a test template. The method will be invoked multiple times with different contexts provided by a
TestTemplateInvocationContextProvider. - TestTemplateInvocationContextProvider: Implement this interface to provide the context for each invocation of the test template. This includes defining the parameters and any setup or teardown logic.
Running the Tests
To run the tests, simply run the CalculatorTest class as a JUnit test. This will execute the test template method multiple times with different contexts.
Using Eclipse
- Run Tests: Right-click on the
CalculatorTestfile and selectRun As>JUnit Test. - View Results: The results will be displayed in the JUnit view, showing the tests executed with different contexts.
Using IntelliJ IDEA
- Run Tests: Click the green run icon next to the
CalculatorTestclass and selectRun. - View Results: The results will be displayed in the Run window, showing the tests executed with different contexts.
Using VS Code
- Run Tests: Open the
CalculatorTestfile and click theRunicon above the class declaration. - View Results: The results will be displayed in the Test Explorer, showing the tests executed with different contexts.
Conclusion
JUnit test templates provide a powerful way to create reusable test structures. By using the @TestTemplate annotation and implementing TestTemplateInvocationContextProvider, you can define test templates that can be invoked with different contexts. This approach helps in avoiding duplication and makes your test code more maintainable, especially for complex testing scenarios.