JUnit Test Templates

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

  1. Run Tests: Right-click on the CalculatorTest file and select Run As > JUnit Test.
  2. View Results: The results will be displayed in the JUnit view, showing the tests executed with different contexts.

Using IntelliJ IDEA

  1. Run Tests: Click the green run icon next to the CalculatorTest class and select Run.
  2. View Results: The results will be displayed in the Run window, showing the tests executed with different contexts.

Using VS Code

  1. Run Tests: Open the CalculatorTest file and click the Run icon above the class declaration.
  2. 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.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top