Skip to content

Commit

Permalink
Support declarative extension registration on fields and parameters
Browse files Browse the repository at this point in the history
Prior to this commit, @ExtendWith could only be used to register
extensions declaratively on test interfaces, test classes, and test
methods. However, there are certain use cases where it is more
convenient for the user if extensions can be registered declaratively
on fields and parameters.

This commit introduces support for registering extensions declaratively
via @ExtendWith on the following elements.

- static fields
- non-static fields
- parameters in test class constructors, test methods, and lifecycle
  methods (i.e., @BeforeAll, @afterall, @beforeeach, and @AfterEach
  methods)

Fields annotated or meta-annotated with @ExtendWith can have any
visibility (including private) and can be sorted relative to
@RegisterExtension fields via the @order annotation.

See the RandomNumberExtension example in the User Guide.

Closes #864, #2680
  • Loading branch information
sbrannen committed Aug 17, 2021
2 parents 3f605f1 + b8e44fa commit 8b5387c
Show file tree
Hide file tree
Showing 15 changed files with 1,314 additions and 172 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ on GitHub.
* Generating Java Flight Recorder events via module `org.junit.platform.jfr` is now also
supported on Java 8 Update 262 or higher, in addition to Java 11 or later. See
<<../user-guide/index.adoc#running-tests, Flight Recorder Support>> for details.
* Suites executed by the `junit-platform-suite` module will now inherit the configuration
parameters from the parent discovery request. This behavior can be disabled via the
`@DisableParentConfigurationParameters` annotation.


[[release-notes-5.8.0-M2-junit-jupiter]]
Expand All @@ -84,7 +87,11 @@ on GitHub.

==== New Features and Improvements

* `assertDoesNotThrow` now supports suspending functions when called from Kotlin.
* New `assertThrowsExactly()` method in `Assertions` which is a more strict version of
`assertThrows()` that allows you to assert that the exception thrown is of the exact
type specified.
* `assertDoesNotThrow()` in `Assertions` now supports suspending functions when called
from Kotlin.
* `@TempDir` can now be used to create multiple temporary directories. Instead of creating
a single temporary directory per context (i.e. test class or method) every declaration
of the `@TempDir` annotation on a field or method parameter now results in a separate
Expand All @@ -93,13 +100,15 @@ on GitHub.
you can set the `junit.jupiter.tempdir.scope` configuration parameter to `per_context`.
* `@TempDir` cleanup resets readable and executable permissions of the root temporary
directory and any contained directories instead of failing to delete them.
* `@TempDir` fields may now be private.
* `@TempDir` fields may now be `private`.
* `@ExtendWith` may now be used to register extensions declaratively via fields or
parameters in test class constructors, test methods, and lifecycle methods. See
<<../user-guide/index.adoc#extensions-registration-declarative, Declarative Extension
Registration>> for details.
* New `named()` static factory method in the `Named` interface that serves as an _alias_
for `Named.of()`. `named()` is intended to be used via `import static`.
* New `class` URI scheme for dynamic test sources. This allows tests to be located using
the information available in a `StackTraceElement`.
* New `assertThrowsExactly` method which is a more strict version of `assertThrows`
that allows you to assert that the thrown exception has the exact specified class.
* New `autoCloseArguments` attribute in `@ParameterizedTest` to close `AutoCloseable`
arguments at the end of the test. This attribute defaults to true.

Expand All @@ -117,6 +126,4 @@ on GitHub.

==== New Features and Improvements

* Suites executed by the `junit-platform-suite` module will now inherit the
configuration parameters from the parent discovery request. This behaviour can
be disabled via the `@DisableParentConfigurationParameters` annotation.
* ❓
143 changes: 103 additions & 40 deletions documentation/src/docs/asciidoc/user-guide/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,32 @@ Java's <<extensions-registration-automatic,`ServiceLoader`>> mechanism.

Developers can register one or more extensions _declaratively_ by annotating a test
interface, test class, test method, or custom _<<writing-tests-meta-annotations,composed
annotation>>_ with `@ExtendWith(...)` and supplying class references for the extensions
to register.
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 custom `RandomParametersExtension` for a particular test
method, you would annotate the test method as follows.
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`.

[source,java,indent=0]
----
@ExtendWith(RandomParametersExtension.class)
@Test
void test(@Random int i) {
// ...
@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 a custom `RandomParametersExtension` for all tests in a particular class and
its subclasses, you would annotate the test class as follows.
To register the `WebServerExtension` for all tests in a particular class and its
subclasses, you would annotate the test class as follows.

[source,java,indent=0]
----
@ExtendWith(RandomParametersExtension.class)
@ExtendWith(WebServerExtension.class)
class MyTests {
// ...
}
Expand Down Expand Up @@ -71,15 +76,16 @@ class MySecondTests {
[TIP]
.Extension Registration Order
====
Extensions registered declaratively via `@ExtendWith` will be executed in the order in
which they are declared in the source code. For example, the execution of tests in both
`MyFirstTests` and `MySecondTests` will be extended by the `DatabaseExtension` and
`WebServerExtension`, **in exactly that order**.
Extensions registered declaratively via `@ExtendWith` at the class level, method level, or
parameter level will be executed in the order in which they are declared in the source
code. For example, the execution of tests in both `MyFirstTests` and `MySecondTests` will
be extended by the `DatabaseExtension` and `WebServerExtension`, **in exactly that order**.
====

If you wish to combine multiple extensions in a reusable way, you can define a custom
_<<writing-tests-meta-annotations,composed annotation>>_ and use `@ExtendWith` as a
_meta-annotation_:
_meta-annotation_ as in the following code listing. Then `@DatabaseAndWebServerExtension`
can be used in place of `@ExtendWith({ DatabaseExtension.class, WebServerExtension.class })`.

[source,java,indent=0]
----
Expand All @@ -90,6 +96,64 @@ 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` that 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 `RandomNumberTests` example.

[source,java,indent=0]
----
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(RandomNumberExtension.class)
public @interface Random {
}
----

[source,java,indent=0]
----
class RandomNumberTests {
// use random number field in test methods and @BeforeEach
// or @AfterEach lifecycle methods
@Random
private int randomNumber1;
RandomNumberTests(@Random int randomNumber2) {
// use random number in constructor
}
@BeforeEach
void beforeEach(@Random int randomNumber3) {
// use random number in @BeforeEach method
}
@Test
void test(@Random int randomNumber4) {
// use random number in test method
}
}
----

[TIP]
.Extension Registration Order for `@ExtendWith` on Fields
====
Extensions registered declaratively via `@ExtendWith` on fields will be ordered relative
to `@RegisterExtension` fields and other `@ExtendWith` fields using an algorithm that is
deterministic but intentionally nonobvious. However, `@ExtendWith` fields can be ordered
using the `@Order` annotation. See the <<extensions-registration-programmatic-order,
Extension Registration Order>> tip for `@RegisterExtension` fields for details.
====

NOTE: `@ExtendWith` fields may be either `static` or non-static. The documentation on
<<extensions-registration-programmatic-static-fields, Static Fields>> and
<<extensions-registration-programmatic-instance-fields, Instance Fields>> for
`@RegisterExtension` fields also applies to `@ExtendWith` fields.

[[extensions-registration-programmatic]]
==== Programmatic Extension Registration

Expand All @@ -106,27 +170,27 @@ extension's constructor, a static factory method, or a builder API.
[TIP]
.Extension Registration Order
====
By default, extensions registered programmatically via `@RegisterExtension` will be
ordered using an algorithm that is deterministic but intentionally nonobvious. This
ensures that subsequent runs of a test suite execute extensions in the same order, thereby
allowing for repeatable builds. However, there are times when extensions need to be
registered in an explicit order. To achieve that, annotate `@RegisterExtension` fields
with `{Order}`.
Any `@RegisterExtension` field not annotated with `@Order` will be ordered using the
_default_ order which has a value of `Integer.MAX_VALUE / 2`. This allows `@Order`
annotated extension fields to be explicitly ordered before or after non-annotated
extension fields. Extensions with an explicit order value less than the default order
value will be registered before non-annotated extensions. Similarly, extensions with an
explicit order value greater than the default order value will be registered after
non-annotated extensions. For example, assigning an extension an explicit order value that
is greater than the default order value allows _before_ callback extensions to be
registered last and _after_ callback extensions to be registered first, relative to other
programmatically registered extensions.
By default, extensions registered programmatically via `@RegisterExtension` or
declaratively via `@ExtendWith` on fields will be ordered using an algorithm that is
deterministic but intentionally nonobvious. This ensures that subsequent runs of a test
suite execute extensions in the same order, thereby allowing for repeatable builds.
However, there are times when extensions need to be registered in an explicit order. To
achieve that, annotate `@RegisterExtension` fields or `@ExtendWith` fields with `{Order}`.
Any `@RegisterExtension` field or `@ExtendWith` field not annotated with `@Order` will be
ordered using the _default_ order which has a value of `Integer.MAX_VALUE / 2`. This
allows `@Order` annotated extension fields to be explicitly ordered before or after
non-annotated extension fields. Extensions with an explicit order value less than the
default order value will be registered before non-annotated extensions. Similarly,
extensions with an explicit order value greater than the default order value will be
registered after non-annotated extensions. For example, assigning an extension an explicit
order value that is greater than the default order value allows _before_ callback
extensions to be registered last and _after_ callback extensions to be registered first,
relative to other programmatically registered extensions.
====

NOTE: `@RegisterExtension` fields must not be `private` or `null` (at evaluation time) but
may be either `static` or non-static.
NOTE: `@RegisterExtension` fields must not be `null` (at evaluation time) but may be
either `static` or non-static.

[[extensions-registration-programmatic-static-fields]]
===== Static Fields
Expand All @@ -149,19 +213,18 @@ lifecycle methods annotated with `@BeforeAll` or `@AfterAll` as well as `@Before
`server` field if necessary.

[source,java,indent=0]
.An extension registered via a static field
.Registering an extension via a static field in Java
----
include::{testDir}/example/registration/WebServerDemo.java[tags=user_guide]
----

[[extensions-registration-programmatic-static-fields-kotlin]]
===== Static Fields in Kotlin
====== Static Fields in Kotlin

The Kotlin programming language does not have the concept of a `static` field. However,
the compiler can be instructed to generate static fields using annotations. Since, as
stated earlier, `@RegisterExtension` fields must not be `private` nor `null`, one
**cannot** use the `@JvmStatic` annotation in Kotlin as it generates `private` fields.
Rather, the `@JvmField` annotation must be used.
the compiler can be instructed to generate a `private static` field using the `@JvmStatic`
annotation in Kotlin. If you want the Kotlin compiler to generate a `public static` field,
you can use the `@JvmField` annotation instead.

The following example is a version of the `WebServerDemo` from the previous section that
has been ported to Kotlin.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import org.junit.jupiter.api.extension.RegisterExtension
class KotlinWebServerDemo {

companion object {
@JvmField
@JvmStatic
@RegisterExtension
val server = WebServerExtension.builder()
.enableSecurity(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,47 @@

/**
* {@code @ExtendWith} is a {@linkplain Repeatable repeatable} annotation
* that is used to register {@linkplain Extension extensions} for the
* annotated test class or test method.
* that is used to register {@linkplain Extension extensions} for the annotated
* test class, test interface, test method, parameter, or field.
*
* <p>Annotated parameters are supported in test class constructors, in test
* methods, and in {@code @BeforeAll}, {@code @AfterAll}, {@code @BeforeEach},
* and {@code @AfterEach} lifecycle methods.
*
* <p>{@code @ExtendWith} fields may be either {@code static} or non-static.
*
* <h3>Inheritance</h3>
*
* <p>{@code @ExtendWith} fields are inherited from superclasses as long as they
* are not <em>hidden</em> or <em>overridden</em>. Furthermore, {@code @ExtendWith}
* fields from superclasses will be registered before {@code @ExtendWith} fields
* in subclasses.
*
* <h3>Registration Order</h3>
*
* <p>When {@code @ExtendWith} is present on a test class, test interface, or
* test method or on a parameter in a test method or lifecycle method, the
* corresponding extensions will be registered in the order in which the
* {@code @ExtendWith} annotations are discovered. For example, if a test class
* is annotated with {@code @ExtendWith(A.class)} and then with
* {@code @ExtendWith(B.class)}, extension {@code A} will be registered before
* extension {@code B}.
*
* <p>By default, if multiple extensions are registered on fields via
* {@code @ExtendWith}, they will be ordered using an algorithm that is
* deterministic but intentionally nonobvious. This ensures that subsequent runs
* of a test suite execute extensions in the same order, thereby allowing for
* repeatable builds. However, there are times when extensions need to be
* registered in an explicit order. To achieve that, you can annotate
* {@code @ExtendWith} fields with {@link org.junit.jupiter.api.Order @Order}.
* Any {@code @ExtendWith} field not annotated with {@code @Order} will be
* ordered using the {@link org.junit.jupiter.api.Order#DEFAULT default} order
* value. Note that {@code @RegisterExtension} fields can also be ordered with
* {@code @Order}, relative to {@code @ExtendWith} fields and other
* {@code @RegisterExtension} fields.
*
* <h3>Supported Extension APIs</h3>
*
* <ul>
* <li>{@link ExecutionCondition}</li>
* <li>{@link InvocationInterceptor}</li>
Expand All @@ -49,7 +86,7 @@
* @see RegisterExtension
* @see Extension
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
* to pass arguments to the extension's constructor, {@code static} factory
* method, or builder API.
*
* <p>{@code @RegisterExtension} fields must not be {@code private} or
* {@code null} (when evaluated) but may be either {@code static} or non-static.
* <p>{@code @RegisterExtension} fields must not be {@code null} (when evaluated)
* but may be either {@code static} or non-static.
*
* <h3>Static Fields</h3>
*
Expand Down Expand Up @@ -81,7 +81,9 @@
* {@code @RegisterExtension} fields with {@link org.junit.jupiter.api.Order @Order}.
* Any {@code @RegisterExtension} field not annotated with {@code @Order} will be
* ordered using the {@link org.junit.jupiter.api.Order#DEFAULT default} order
* value.
* value. Note that {@code @ExtendWith} fields can also be ordered with
* {@code @Order}, relative to {@code @RegisterExtension} fields and other
* {@code @ExtendWith} fields.
*
* <h3>Example Usage</h3>
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import static java.util.stream.Collectors.joining;
import static org.apiguardian.api.API.Status.INTERNAL;
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.populateNewExtensionRegistryFromExtendWithAnnotation;
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromConstructorParameters;
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromExecutableParameters;
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromFields;
import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.findAfterAllMethods;
import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.findAfterEachMethods;
Expand Down Expand Up @@ -152,16 +154,27 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte
// one factory registered per class).
this.testInstanceFactory = resolveTestInstanceFactory(registry);

if (this.testInstanceFactory == null) {
registerExtensionsFromConstructorParameters(registry, this.testClass);
}

this.beforeAllMethods = findBeforeAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD);
this.afterAllMethods = findAfterAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD);

this.beforeAllMethods.forEach(method -> registerExtensionsFromExecutableParameters(registry, method));
// Since registerBeforeEachMethodAdapters() and registerAfterEachMethodAdapters() also
// invoke registerExtensionsFromExecutableParameters(), we invoke those methods before
// invoking registerExtensionsFromExecutableParameters() for @AfterAll methods,
// thereby ensuring proper registration order for extensions registered via @ExtendWith
// on parameters in lifecycle methods.
registerBeforeEachMethodAdapters(registry);
registerAfterEachMethodAdapters(registry);
this.afterAllMethods.forEach(method -> registerExtensionsFromExecutableParameters(registry, method));

ThrowableCollector throwableCollector = createThrowableCollector();
ClassExtensionContext extensionContext = new ClassExtensionContext(context.getExtensionContext(),
context.getExecutionListener(), this, this.lifecycle, context.getConfiguration(), throwableCollector);

this.beforeAllMethods = findBeforeAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD);
this.afterAllMethods = findAfterAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD);

// @formatter:off
return context.extend()
.withTestInstancesProvider(testInstancesProvider(context, extensionContext))
Expand Down Expand Up @@ -468,7 +481,10 @@ private void registerAfterEachMethodAdapters(ExtensionRegistrar registrar) {
private void registerMethodsAsExtensions(List<Method> methods, ExtensionRegistrar registrar,
Function<Method, Extension> extensionSynthesizer) {

methods.forEach(method -> registrar.registerSyntheticExtension(extensionSynthesizer.apply(method), method));
methods.forEach(method -> {
registerExtensionsFromExecutableParameters(registrar, method);
registrar.registerSyntheticExtension(extensionSynthesizer.apply(method), method);
});
}

private BeforeEachMethodAdapter synthesizeBeforeEachMethodAdapter(Method method) {
Expand Down
Loading

0 comments on commit 8b5387c

Please sign in to comment.