Labs ICT
โญ Pro Login

Custom Extensions

JUnit 5's extension model is one of its biggest improvements over JUnit 4. Extensions let you hook into the test lifecycle, add custom behavior, and share logic across test classes. Think of them as plugins for your test framework.

Extension Points


JUnit 5 provides these extension points:

+--------------------------------+----------------------------------------+
|  Interface                     |  Purpose                               |
+--------------------------------+----------------------------------------+
|  BeforeAllCallback             |  Runs before all tests in a class      |
|  AfterAllCallback              |  Runs after all tests in a class       |
|  BeforeEachCallback            |  Runs before each test method          |
|  AfterEachCallback             |  Runs after each test method           |
|  BeforeTestExecutionCallback   |  Runs right before test execution      |
|  AfterTestExecutionCallback    |  Runs right after test execution       |
|  TestInstancePostProcessor     |  Process test instance after creation  |
|  TestWatcher                   |  Watches test execution                |
|  ParameterResolver             |  Resolves test method parameters       |
|  ExceptionHandler             |  Handles exceptions during tests       |
+--------------------------------+----------------------------------------+
    

Your First Extension


import org.junit.jupiter.api.extension.*;
import java.lang.annotation.*;

public class TimingExtension implements BeforeTestExecutionCallback,
        AfterTestExecutionCallback {

    private static final String START_TIME = "start time";

    @Override
    public void beforeTestExecution(ExtensionContext context) {
        context.getStore(ExtensionContext.Namespace.GLOBAL)
            .put(START_TIME, System.nanoTime());
    }

    @Override
    public void afterTestExecution(ExtensionContext context) {
        long startTime = context.getStore(ExtensionContext.Namespace.GLOBAL)
            .get(START_TIME, Long.class);
        long duration = System.nanoTime() - startTime;

        String testName = context.getDisplayName();
        System.out.printf("Test %s took %.2f ms%n",
            testName, duration / 1_000_000.0);
    }
}
    

// Register the extension
@ExtendWith(TimingExtension.class)
class MyTest {

    @Test
    void shouldRunQuickly() {
        // After this test, TimingExtension prints duration
    }
}
    

Annotation-Driven Extensions

The most powerful pattern is creating a custom annotation that registers an extension:


// Step 1: Create the annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(TimingExtension.class)
public @interface Timed {
}

// Step 2: Use it
class MyTest {

    @Test
    @Timed
    void someTest() {
        // This test is automatically timed
    }
}
    

ParameterResolver Extension


public class RandomAnnotation
    implements ParameterResolver {

    @Override
    public boolean supportsParameter(
            ParameterContext context,
            ExtensionContext extensionContext) {
        return context.isAnnotated(Random.class);
    }

    @Override
    public Object resolveParameter(
            ParameterContext context,
            ExtensionContext extensionContext) {
        Random random = context.findAnnotation(Random.class).get();
        return (int) (Math.random() * random.max()) + 1;
    }
}

// Usage
@Test
void randomTest(@Random(max = 100) int number) {
    assertTrue(number > 0 && number <= 100);
}
    

TemporaryDirectory Extension


import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Path;

class FileTest {

    @TempDir
    Path tempDir;

    @Test
    void shouldWriteToFile() throws IOException {
        Path file = tempDir.resolve("test.txt");
        Files.writeString(file, "hello");

        assertEquals("hello", Files.readString(file));
    }

    // JUnit automatically cleans up the directory
}
    

Conditional Extensions


public class LinuxOnlyCondition implements ExecutionCondition {

    @Override
    public ConditionEvaluationResult evaluateExecutionCondition(
            ExtensionContext context) {
        String os = System.getProperty("os.name").toLowerCase();
        if (os.contains("linux")) {
            return ConditionEvaluationResult.enabled("Running on Linux");
        }
        return ConditionEvaluationResult.disabled("Not on Linux, skipping");
    }
}

// Create an annotation for it
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(LinuxOnlyCondition.class)
public @interface LinuxOnly {
}

// Usage
@LinuxOnly
class LinuxSpecificTest {
    @Test
    void linuxTest() {
        // Only runs on Linux
    }
}
    

Registered Extensions (Global)

Instead of annotating every test class, register extensions globally:


// META-INF/services/org.junit.jupiter.api.extension.Extension
com.example.TimingExtension
com.example.LoggingExtension
    

Or in junit-platform.properties:


junit.jupiter.extensions.enabled=true
junit.jupiter.extensions.autodetection.enabled=true
    

Extension Store


public class DatabaseExtension implements
        BeforeEachCallback, AfterEachCallback {

    @Override
    public void beforeEach(ExtensionContext context) {
        Connection conn = createConnection();
        getStore(context).put("connection", conn);
    }

    @Override
    public void afterEach(ExtensionContext context) {
        Connection conn = getStore(context)
            .get("connection", Connection.class);
        conn.close();
    }

    private Store getStore(ExtensionContext context) {
        return context.getStore(
            ExtensionContext.Namespace.create(DatabaseExtension.class)
        );
    }
}
    

๐Ÿงช Quick Quiz

Which JUnit 5 extension point is used to customize test execution?