JUnit 5 Extension Model
Extension Model
JUnit Jupiter provides an extension model that makes it easy to introduce extension features.
Basic Extension Model
To create an extension, implement the Extension interface or one of its subinterfaces. Extension itself is a marker interface and does not define methods.
A marker interface is an interface with no declared methods. Java examples include Serializable, Cloneable, and EventListener. They are commonly used for simple type checks.
Extension points are provided as subinterfaces of Extension, such as BeforeEachCallback and AfterEachCallback.
Use @ExtendWith to apply an extension class to a test class or test method. Applying it to a class affects each test in that class; applying it to a method affects only that method.
Extension Points
JUnit Jupiter provides extension points for execution conditions, test instance creation, post-processing, parameter resolution, lifecycle callbacks, test result watching, exception handling, test templates, programmatic registration, automatic registration, and data sharing.
Support Classes
When implementing extensions, JUnit provides utility classes that simplify common operations and exception handling. They are public support APIs prepared for third-party TestEngine and extension authors, so they can be used safely.
Controlling Test Execution Conditions
Implement ExecutionCondition and return a ConditionEvaluationResult from evaluateExecutionCondition(ExtensionContext). The returned value determines whether the target test is enabled or disabled. This callback is invoked whenever a relevant test is evaluated.
Creating Test Instances
Implement TestInstanceFactory to customize how test instances are created. createTestInstance() is called before each test method is executed and can use reflection support to create the object.
Post-Processing Test Instances
Implement TestInstancePostProcessor to initialize test instances after creation. This is useful for dependency injection or invoking initialization methods.
Parameter Resolution
Implement ParameterResolver to provide arguments to test methods, lifecycle methods, constructors, or dynamic tests. supportsParameter() checks whether a parameter is supported, and resolveParameter() returns the actual value. To read Java parameter names, compile with the -parameters option.
Lifecycle Callbacks
Lifecycle callback interfaces let an extension run logic around JUnit lifecycle points such as before all tests, before each test, before test execution, after test execution, after each test, and after all tests.
Processing Test Results
Implement TestWatcher to react to test outcomes such as success, failure, abort, or disabled state. Its methods are default methods with empty bodies and run after AfterEachCallback.
Handling Exceptions from Tests
Implement TestExecutionExceptionHandler to handle exceptions thrown from test methods. Assertion errors are also targets, so handle them carefully.
Handling Exceptions from Lifecycle Methods
Use LifecycleMethodExecutionExceptionHandler to handle exceptions thrown from lifecycle methods such as @BeforeAll, @BeforeEach, @AfterEach, and @AfterAll. This is separate from TestExecutionExceptionHandler, which handles exceptions from test execution.
Running the Same Test in Different Contexts
Use TestTemplateInvocationContextProvider with @TestTemplate to execute the same test in different invocation contexts. supportsTestTemplate() decides support, and provideTestTemplateInvocationContexts() provides the contexts. Each context can define a display name and additional extensions.
Programmatic Extension Registration
@RegisterExtension registers an extension through a field on the test class. This lets you configure extension instances dynamically. Static fields can register class-level extensions, while instance fields cannot use some class-level callbacks such as BeforeAllCallback.
Automatic Registration with ServiceLoader
Extensions can be registered automatically through Java’s ServiceLoader. Create /META-INF/services/org.junit.jupiter.api.extension.Extension and list extension implementation classes. Enable auto-detection with:
junit.jupiter.extensions.autodetection.enabled=true
Sharing Data Between Extensions
Avoid sharing extension state through instance fields when tests may run in parallel, because the same extension instance can be reused across tests. Instead, use ExtensionContext.Store.
Store is obtained from an ExtensionContext with a Namespace. Separate namespaces prevent key collisions between extensions. Use Namespace.GLOBAL only when data should be shared across all extensions.
Store Lifecycle and Lookup
A Store has the same lifecycle as the ExtensionContext from which it is obtained. Method-level extension contexts have parent class-level contexts, and data stored in a parent context can be found from child contexts.
Cleanup at Lifecycle End
Objects stored in Store can implement ExtensionContext.Store.CloseableResource. Their close() method is called automatically when the corresponding store lifecycle ends, making it useful for cleanup logic.