The Jupiter extension model allows developers to inject custom behavior into the test lifecycle. Unlike JUnit 4's Rules and Runners, the Extension API is modular and allows multiple extensions to be composed together using the @ExtendWith annotation. This annotation is used on a test class (or test method) to register one or more extensions to be applied.
Java source and doc
Definition of ExtendWithVersion: 6.0.1 package org.junit.jupiter.api.extension;
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Repeatable(Extensions.class)
@API(status = STABLE, since = "5.0")
public @interface ExtendWith {
Class<? extends Extension>[] value(); 1
}
What is Extension?
Extension is a marker interface that identifies a class as a JUnit Jupiter extension. By itself, it does not declare any methods.
Widely-used third-party @ExtendWith extensions
Let's see some famous JUnit Jupiter extensions to get an idea what they are capable of. These third-party extensions demonstrate the real-world power and flexibility of JUnit 5's extension model, showing how various testing concerns can be cleanly separated and reused across projects.
Spring Boot Test
@ExtendWith(SpringExtension.class)
@SpringBootTest
class SpringIntegrationTest {
// Automatically loads Spring context
// Provides dependency injection
}
Mockito
@ExtendWith(MockitoExtension.class)
class MockitoTest {
@Mock
private Service service;
@InjectMocks
private Controller controller;
}
Testcontainers
@Testcontainers
@ExtendWith(TestcontainersExtension.class)
class DatabaseTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15");
}
Example: Building a Custom Extension
One of the most common type of Extension is ParameterResolver. By implementing the ParameterResolver interface, an extension can dynamically provide arguments to test methods at runtime.
Definition of ParameterResolverVersion: 6.0.1 package org.junit.jupiter.api.extension;
@API(status = STABLE, since = "5.0")
public interface ParameterResolver extends TestInstantiationAwareExtension {
boolean supportsParameter(ParameterContext parameterContext, 1
ExtensionContext extensionContext)
throws ParameterResolutionException;
Object resolveParameter(ParameterContext parameterContext, 2
ExtensionContext extensionContext)
throws ParameterResolutionException;
}
As seen above, ParameterResolver extends TestInstantiationAwareExtension which extends Extension interface.
Both of the above methods in ParameterResolver interface have a parameter ExtensionContext, the purpose of this parameter is to give extensions access to details about the currently executing test, including the test class, test method, display name, tags, and test instance.
In this example, we will create a custom extension, PageInjectionExtension. It will look for parameters annotated with @Uri and injects a mock Page object initialized with that specific path.
The Custom Extension Implementation
package com.logicbig.example;
import org.junit.jupiter.api.extension.*;
public class PageInjectionExtension implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext pc, ExtensionContext ec) {
return pc.isAnnotated(Uri.class) && pc.getParameter().getType() == Page.class;
}
@Override
public Object resolveParameter(ParameterContext pc, ExtensionContext ec) {
String path = pc.findAnnotation(Uri.class).get().value();
//simulate finding Page from the backend
return new Page(path, "Content of " + path);
}
}
The custom annotation
package com.logicbig.example;
import java.lang.annotation.*;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Uri {
String value();
}
The Custom Parameter Type
public record Page(String path, String content) {}
Test Class
package com.logicbig.example;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ExtendWith(PageInjectionExtension.class)
public class PageInjectionTest {
@Test
void testPageLoad(@Uri("/example/my-page.html") Page page) {
System.out.println("Page: " + page);
assertNotNull(page);
assertEquals("/example/my-page.html", page.path());
assertNotNull(page.content());
}
}
Output$ mvn test -Dtest=PageInjectionTest [INFO] Scanning for projects... [INFO] [INFO] -----< com.logicbig.example:junit-5-extension-parameter-resolver >------ [INFO] Building junit-5-extension-parameter-resolver 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-extension-parameter-resolver --- [WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-extensions\junit-5-extension-parameter-resolver\src\main\resources [INFO] [INFO] --- compiler:3.13.0:compile (default-compile) @ junit-5-extension-parameter-resolver --- [INFO] No sources to compile [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ junit-5-extension-parameter-resolver --- [WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-extensions\junit-5-extension-parameter-resolver\src\test\resources [INFO] [INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ junit-5-extension-parameter-resolver --- [INFO] Nothing to compile - all classes are up to date. [INFO] [INFO] --- surefire:3.5.0:test (default-test) @ junit-5-extension-parameter-resolver --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- Page: Page[path=/example/my-page.html, content=Content of /example/my-page.html] [INFO] +--com.logicbig.example.PageInjectionTest - 0.094 ss [INFO] | '-- [OK] testPageLoad(Page) - 0.067 ss [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.324 s [INFO] Finished at: 2025-12-29T15:15:58+08:00 [INFO] ------------------------------------------------------------------------
The output confirms that the ParameterResolver successfully intercepted the test method signature. By reading the @Uri value, the extension instantiated the Page object with the correct path, allowing the test to verify content specific to that URI without manual setup boilerplate.
Example ProjectDependencies and Technologies Used: - junit-jupiter-engine 6.0.1 (Module "junit-jupiter-engine" of JUnit)
Version Compatibility: 5.1.1 - 6.0.1 Version compatibilities of junit-jupiter-engine with this example:
- 5.1.1
- 5.2.0
- 5.3.0
- 5.3.1
- 5.3.2
- 5.4.0
- 5.4.1
- 5.4.2
- 5.5.0
- 5.5.1
- 5.5.2
- 5.6.0
- 5.6.1
- 5.6.2
- 5.6.3
- 5.7.0
- 5.7.1
- 5.7.2
- 5.8.0
- 5.8.1
- 5.8.2
- 5.9.0
- 5.9.1
- 5.9.2
- 5.9.3
- 5.10.0
- 5.10.1
- 5.10.2
- 5.10.3
- 5.10.4
- 5.10.5
- 5.11.0
- 5.11.1
- 5.11.2
- 5.11.3
- 5.11.4
- 5.12.0
- 5.12.1
- 5.12.2
- 5.13.0
- 5.13.1
- 5.13.2
- 5.13.3
- 5.13.4
- 5.14.0
- 5.14.1
- 6.0.0
- 6.0.1
Versions in green have been tested.
- JDK 25
- Maven 3.9.11
|