Registering Extensions
Extensions can be registered declaratively via
@ExtendWith, programmatically via
@RegisterExtension, or automatically via
Java’s ServiceLoader mechanism.
Declarative Extension Registration
Developers can register one or more extensions declaratively by annotating a test
interface, test class, test method, or custom composed
annotation with @ExtendWith(…) and supplying class references for the extensions to
register. As of JUnit Jupiter 5.8, @ExtendWith may also be declared on fields or on
parameters in test class constructors, in test methods, and in @BeforeAll, @AfterAll,
@BeforeEach, and @AfterEach lifecycle methods.
For example, to register a WebServerExtension for a particular test method, you would
annotate the test method as follows. We assume the WebServerExtension starts a local web
server and injects the server’s URL into parameters annotated with @WebServerUrl.
@Test
@ExtendWith(WebServerExtension.class)
void getProductList(@WebServerUrl String serverUrl) {
WebClient webClient = new WebClient();
// Use WebClient to connect to web server using serverUrl and verify response
assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
}
To register the WebServerExtension for all tests in a particular class and its
subclasses, you would annotate the test class as follows.
@ExtendWith(WebServerExtension.class)
class MyTests {
// ...
}
Multiple extensions can be registered together like this:
@ExtendWith({ DatabaseExtension.class, WebServerExtension.class })
class MyFirstTests {
// ...
}
As an alternative, multiple extensions can be registered separately like this:
@ExtendWith(DatabaseExtension.class)
@ExtendWith(WebServerExtension.class)
class MySecondTests {
// ...
}
|
Extension Registration Order
Extensions registered declaratively via |
If you wish to combine multiple extensions in a reusable way, you can define a custom
composed annotation and use @ExtendWith as a
meta-annotation as in the following code listing. Then @DatabaseAndWebServerExtension
can be used in place of @ExtendWith({ DatabaseExtension.class, WebServerExtension.class }).
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith({ DatabaseExtension.class, WebServerExtension.class })
public @interface DatabaseAndWebServerExtension {
}
The above examples demonstrate how @ExtendWith can be applied at the class level or at
the method level; however, for certain use cases it makes sense for an extension to be
registered declaratively at the field or parameter level. Consider a
RandomNumberExtension which generates random numbers that can be injected into a field or
via a parameter in a constructor, test method, or lifecycle method. If the extension
provides a @Random annotation that is meta-annotated with
@ExtendWith(RandomNumberExtension.class) (see listing below), the extension can be used
transparently as in the following RandomNumberDemo example.
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(RandomNumberExtension.class)
public @interface Random {
}
class RandomNumberDemo {
// Use static randomNumber0 field anywhere in the test class,
// including @BeforeAll or @AfterEach lifecycle methods.
@Random
private static Integer randomNumber0;
// Use randomNumber1 field in test methods and @BeforeEach
// or @AfterEach lifecycle methods.
@Random
private int randomNumber1;
RandomNumberDemo(@Random int randomNumber2) {
// Use randomNumber2 in constructor.
}
@BeforeEach
void beforeEach(@Random int randomNumber3) {
// Use randomNumber3 in @BeforeEach method.
}
@Test
void test(@Random int randomNumber4) {
// Use randomNumber4 in test method.
}
}
The following code listing provides an example of how one might choose to implement such a
RandomNumberExtension. This implementation works for the use cases in
RandomNumberDemo; however, it may not prove robust enough to cover all use cases — for
example, the random number generation support is limited to integers; it uses
java.util.Random instead of java.security.SecureRandom; etc. In any case, it is
important to note which extension APIs are implemented and for what reasons.
Specifically, RandomNumberExtension implements the following extension APIs:
-
BeforeAllCallback: to support static field injection -
TestInstancePostProcessor: to support non-static field injection -
ParameterResolver: to support constructor and method injection
import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedFields;
import java.lang.reflect.Field;
import java.util.function.Predicate;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
import org.junit.platform.commons.support.ModifierSupport;
class RandomNumberExtension
implements BeforeAllCallback, TestInstancePostProcessor, ParameterResolver {
private final java.util.Random random = new java.util.Random(System.nanoTime());
/**
* Inject a random integer into static fields that are annotated with
* {@code @Random} and can be assigned an integer value.
*/
@Override
public void beforeAll(ExtensionContext context) {
Class<?> testClass = context.getRequiredTestClass();
injectFields(testClass, null, ModifierSupport::isStatic);
}
/**
* Inject a random integer into non-static fields that are annotated with
* {@code @Random} and can be assigned an integer value.
*/
@Override
public void postProcessTestInstance(Object testInstance, ExtensionContext context) {
Class<?> testClass = context.getRequiredTestClass();
injectFields(testClass, testInstance, ModifierSupport::isNotStatic);
}
/**
* Determine if the parameter is annotated with {@code @Random} and can be
* assigned an integer value.
*/
@Override
public boolean supportsParameter(ParameterContext pc, ExtensionContext ec) {
return pc.isAnnotated(Random.class) && isInteger(pc.getParameter().getType());
}
/**
* Resolve a random integer.
*/
@Override
public Integer resolveParameter(ParameterContext pc, ExtensionContext ec) {
return this.random.nextInt();
}
private void injectFields(Class<?> testClass, Object testInstance,
Predicate<Field> predicate) {
predicate = predicate.and(field -> isInteger(field.getType()));
findAnnotatedFields(testClass, Random.class, predicate)
.forEach(field -> {
try {
field.setAccessible(true);
field.set(testInstance, this.random.nextInt());
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
});
}
private static boolean isInteger(Class<?> type) {
return type == Integer.class || type == int.class;
}
}
|
Extension Registration Order for
@ExtendWith on FieldsExtensions registered declaratively via |
|
Extension Inheritance
Extensions registered declaratively via See Extension Inheritance for details. |
@ExtendWith fields may be either static or non-static. The documentation on
Static Fields and
Instance Fields for
@RegisterExtension fields also applies to @ExtendWith fields.
|