Skip to content

Commit 8b5387c

Browse files
committed
Support declarative extension registration on fields and parameters
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
2 parents 3f605f1 + b8e44fa commit 8b5387c

File tree

15 files changed

+1314
-172
lines changed

15 files changed

+1314
-172
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.8.0-M2.adoc

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ on GitHub.
5858
* Generating Java Flight Recorder events via module `org.junit.platform.jfr` is now also
5959
supported on Java 8 Update 262 or higher, in addition to Java 11 or later. See
6060
<<../user-guide/index.adoc#running-tests, Flight Recorder Support>> for details.
61+
* Suites executed by the `junit-platform-suite` module will now inherit the configuration
62+
parameters from the parent discovery request. This behavior can be disabled via the
63+
`@DisableParentConfigurationParameters` annotation.
6164

6265

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

8588
==== New Features and Improvements
8689

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

@@ -117,6 +126,4 @@ on GitHub.
117126

118127
==== New Features and Improvements
119128

120-
* Suites executed by the `junit-platform-suite` module will now inherit the
121-
configuration parameters from the parent discovery request. This behaviour can
122-
be disabled via the `@DisableParentConfigurationParameters` annotation.
129+
* ❓

documentation/src/docs/asciidoc/user-guide/extensions.adoc

Lines changed: 103 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,32 @@ Java's <<extensions-registration-automatic,`ServiceLoader`>> mechanism.
2121

2222
Developers can register one or more extensions _declaratively_ by annotating a test
2323
interface, test class, test method, or custom _<<writing-tests-meta-annotations,composed
24-
annotation>>_ with `@ExtendWith(...)` and supplying class references for the extensions
25-
to register.
24+
annotation>>_ with `@ExtendWith(...)` and supplying class references for the extensions to
25+
register. As of JUnit Jupiter 5.8, `@ExtendWith` may also be declared on fields or on
26+
parameters in test class constructors, in test methods, and in `@BeforeAll`, `@AfterAll`,
27+
`@BeforeEach`, and `@AfterEach` lifecycle methods.
2628

27-
For example, to register a custom `RandomParametersExtension` for a particular test
28-
method, you would annotate the test method as follows.
29+
For example, to register a `WebServerExtension` for a particular test method, you would
30+
annotate the test method as follows. We assume the `WebServerExtension` starts a local web
31+
server and injects the server's URL into parameters annotated with `@WebServerUrl`.
2932

3033
[source,java,indent=0]
3134
----
32-
@ExtendWith(RandomParametersExtension.class)
3335
@Test
34-
void test(@Random int i) {
35-
// ...
36+
@ExtendWith(WebServerExtension.class)
37+
void getProductList(@WebServerUrl String serverUrl) {
38+
WebClient webClient = new WebClient();
39+
// Use WebClient to connect to web server using serverUrl and verify response
40+
assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
3641
}
3742
----
3843

39-
To register a custom `RandomParametersExtension` for all tests in a particular class and
40-
its subclasses, you would annotate the test class as follows.
44+
To register the `WebServerExtension` for all tests in a particular class and its
45+
subclasses, you would annotate the test class as follows.
4146

4247
[source,java,indent=0]
4348
----
44-
@ExtendWith(RandomParametersExtension.class)
49+
@ExtendWith(WebServerExtension.class)
4550
class MyTests {
4651
// ...
4752
}
@@ -71,15 +76,16 @@ class MySecondTests {
7176
[TIP]
7277
.Extension Registration Order
7378
====
74-
Extensions registered declaratively via `@ExtendWith` will be executed in the order in
75-
which they are declared in the source code. For example, the execution of tests in both
76-
`MyFirstTests` and `MySecondTests` will be extended by the `DatabaseExtension` and
77-
`WebServerExtension`, **in exactly that order**.
79+
Extensions registered declaratively via `@ExtendWith` at the class level, method level, or
80+
parameter level will be executed in the order in which they are declared in the source
81+
code. For example, the execution of tests in both `MyFirstTests` and `MySecondTests` will
82+
be extended by the `DatabaseExtension` and `WebServerExtension`, **in exactly that order**.
7883
====
7984

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

8490
[source,java,indent=0]
8591
----
@@ -90,6 +96,64 @@ public @interface DatabaseAndWebServerExtension {
9096
}
9197
----
9298

99+
The above examples demonstrate how `@ExtendWith` can be applied at the class level or at
100+
the method level; however, for certain use cases it makes sense for an extension to be
101+
registered declaratively at the field or parameter level. Consider a
102+
`RandomNumberExtension` that generates random numbers that can be injected into a field or
103+
via a parameter in a constructor, test method, or lifecycle method. If the extension
104+
provides a `@Random` annotation that is meta-annotated with
105+
`@ExtendWith(RandomNumberExtension.class)` (see listing below), the extension can be used
106+
transparently as in the following `RandomNumberTests` example.
107+
108+
[source,java,indent=0]
109+
----
110+
@Target({ ElementType.FIELD, ElementType.PARAMETER })
111+
@Retention(RetentionPolicy.RUNTIME)
112+
@ExtendWith(RandomNumberExtension.class)
113+
public @interface Random {
114+
}
115+
----
116+
117+
[source,java,indent=0]
118+
----
119+
class RandomNumberTests {
120+
121+
// use random number field in test methods and @BeforeEach
122+
// or @AfterEach lifecycle methods
123+
@Random
124+
private int randomNumber1;
125+
126+
RandomNumberTests(@Random int randomNumber2) {
127+
// use random number in constructor
128+
}
129+
130+
@BeforeEach
131+
void beforeEach(@Random int randomNumber3) {
132+
// use random number in @BeforeEach method
133+
}
134+
135+
@Test
136+
void test(@Random int randomNumber4) {
137+
// use random number in test method
138+
}
139+
}
140+
----
141+
142+
[TIP]
143+
.Extension Registration Order for `@ExtendWith` on Fields
144+
====
145+
Extensions registered declaratively via `@ExtendWith` on fields will be ordered relative
146+
to `@RegisterExtension` fields and other `@ExtendWith` fields using an algorithm that is
147+
deterministic but intentionally nonobvious. However, `@ExtendWith` fields can be ordered
148+
using the `@Order` annotation. See the <<extensions-registration-programmatic-order,
149+
Extension Registration Order>> tip for `@RegisterExtension` fields for details.
150+
====
151+
152+
NOTE: `@ExtendWith` fields may be either `static` or non-static. The documentation on
153+
<<extensions-registration-programmatic-static-fields, Static Fields>> and
154+
<<extensions-registration-programmatic-instance-fields, Instance Fields>> for
155+
`@RegisterExtension` fields also applies to `@ExtendWith` fields.
156+
93157
[[extensions-registration-programmatic]]
94158
==== Programmatic Extension Registration
95159

@@ -106,27 +170,27 @@ extension's constructor, a static factory method, or a builder API.
106170
[TIP]
107171
.Extension Registration Order
108172
====
109-
By default, extensions registered programmatically via `@RegisterExtension` will be
110-
ordered using an algorithm that is deterministic but intentionally nonobvious. This
111-
ensures that subsequent runs of a test suite execute extensions in the same order, thereby
112-
allowing for repeatable builds. However, there are times when extensions need to be
113-
registered in an explicit order. To achieve that, annotate `@RegisterExtension` fields
114-
with `{Order}`.
115-
116-
Any `@RegisterExtension` field not annotated with `@Order` will be ordered using the
117-
_default_ order which has a value of `Integer.MAX_VALUE / 2`. This allows `@Order`
118-
annotated extension fields to be explicitly ordered before or after non-annotated
119-
extension fields. Extensions with an explicit order value less than the default order
120-
value will be registered before non-annotated extensions. Similarly, extensions with an
121-
explicit order value greater than the default order value will be registered after
122-
non-annotated extensions. For example, assigning an extension an explicit order value that
123-
is greater than the default order value allows _before_ callback extensions to be
124-
registered last and _after_ callback extensions to be registered first, relative to other
125-
programmatically registered extensions.
173+
By default, extensions registered programmatically via `@RegisterExtension` or
174+
declaratively via `@ExtendWith` on fields will be ordered using an algorithm that is
175+
deterministic but intentionally nonobvious. This ensures that subsequent runs of a test
176+
suite execute extensions in the same order, thereby allowing for repeatable builds.
177+
However, there are times when extensions need to be registered in an explicit order. To
178+
achieve that, annotate `@RegisterExtension` fields or `@ExtendWith` fields with `{Order}`.
179+
180+
Any `@RegisterExtension` field or `@ExtendWith` field not annotated with `@Order` will be
181+
ordered using the _default_ order which has a value of `Integer.MAX_VALUE / 2`. This
182+
allows `@Order` annotated extension fields to be explicitly ordered before or after
183+
non-annotated extension fields. Extensions with an explicit order value less than the
184+
default order value will be registered before non-annotated extensions. Similarly,
185+
extensions with an explicit order value greater than the default order value will be
186+
registered after non-annotated extensions. For example, assigning an extension an explicit
187+
order value that is greater than the default order value allows _before_ callback
188+
extensions to be registered last and _after_ callback extensions to be registered first,
189+
relative to other programmatically registered extensions.
126190
====
127191

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

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

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

157221
[[extensions-registration-programmatic-static-fields-kotlin]]
158-
===== Static Fields in Kotlin
222+
====== Static Fields in Kotlin
159223

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

166229
The following example is a version of the `WebServerDemo` from the previous section that
167230
has been ported to Kotlin.

documentation/src/test/kotlin/example/registration/KotlinWebServerDemo.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import org.junit.jupiter.api.extension.RegisterExtension
1717
class KotlinWebServerDemo {
1818

1919
companion object {
20-
@JvmField
20+
@JvmStatic
2121
@RegisterExtension
2222
val server = WebServerExtension.builder()
2323
.enableSecurity(false)

junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtendWith.java

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,47 @@
2424

2525
/**
2626
* {@code @ExtendWith} is a {@linkplain Repeatable repeatable} annotation
27-
* that is used to register {@linkplain Extension extensions} for the
28-
* annotated test class or test method.
27+
* that is used to register {@linkplain Extension extensions} for the annotated
28+
* test class, test interface, test method, parameter, or field.
29+
*
30+
* <p>Annotated parameters are supported in test class constructors, in test
31+
* methods, and in {@code @BeforeAll}, {@code @AfterAll}, {@code @BeforeEach},
32+
* and {@code @AfterEach} lifecycle methods.
33+
*
34+
* <p>{@code @ExtendWith} fields may be either {@code static} or non-static.
35+
*
36+
* <h3>Inheritance</h3>
37+
*
38+
* <p>{@code @ExtendWith} fields are inherited from superclasses as long as they
39+
* are not <em>hidden</em> or <em>overridden</em>. Furthermore, {@code @ExtendWith}
40+
* fields from superclasses will be registered before {@code @ExtendWith} fields
41+
* in subclasses.
42+
*
43+
* <h3>Registration Order</h3>
44+
*
45+
* <p>When {@code @ExtendWith} is present on a test class, test interface, or
46+
* test method or on a parameter in a test method or lifecycle method, the
47+
* corresponding extensions will be registered in the order in which the
48+
* {@code @ExtendWith} annotations are discovered. For example, if a test class
49+
* is annotated with {@code @ExtendWith(A.class)} and then with
50+
* {@code @ExtendWith(B.class)}, extension {@code A} will be registered before
51+
* extension {@code B}.
52+
*
53+
* <p>By default, if multiple extensions are registered on fields via
54+
* {@code @ExtendWith}, they will be ordered using an algorithm that is
55+
* deterministic but intentionally nonobvious. This ensures that subsequent runs
56+
* of a test suite execute extensions in the same order, thereby allowing for
57+
* repeatable builds. However, there are times when extensions need to be
58+
* registered in an explicit order. To achieve that, you can annotate
59+
* {@code @ExtendWith} fields with {@link org.junit.jupiter.api.Order @Order}.
60+
* Any {@code @ExtendWith} field not annotated with {@code @Order} will be
61+
* ordered using the {@link org.junit.jupiter.api.Order#DEFAULT default} order
62+
* value. Note that {@code @RegisterExtension} fields can also be ordered with
63+
* {@code @Order}, relative to {@code @ExtendWith} fields and other
64+
* {@code @RegisterExtension} fields.
2965
*
3066
* <h3>Supported Extension APIs</h3>
67+
*
3168
* <ul>
3269
* <li>{@link ExecutionCondition}</li>
3370
* <li>{@link InvocationInterceptor}</li>
@@ -49,7 +86,7 @@
4986
* @see RegisterExtension
5087
* @see Extension
5188
*/
52-
@Target({ ElementType.TYPE, ElementType.METHOD })
89+
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER })
5390
@Retention(RetentionPolicy.RUNTIME)
5491
@Documented
5592
@Inherited

junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/RegisterExtension.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
* to pass arguments to the extension's constructor, {@code static} factory
3131
* method, or builder API.
3232
*
33-
* <p>{@code @RegisterExtension} fields must not be {@code private} or
34-
* {@code null} (when evaluated) but may be either {@code static} or non-static.
33+
* <p>{@code @RegisterExtension} fields must not be {@code null} (when evaluated)
34+
* but may be either {@code static} or non-static.
3535
*
3636
* <h3>Static Fields</h3>
3737
*
@@ -81,7 +81,9 @@
8181
* {@code @RegisterExtension} fields with {@link org.junit.jupiter.api.Order @Order}.
8282
* Any {@code @RegisterExtension} field not annotated with {@code @Order} will be
8383
* ordered using the {@link org.junit.jupiter.api.Order#DEFAULT default} order
84-
* value.
84+
* value. Note that {@code @ExtendWith} fields can also be ordered with
85+
* {@code @Order}, relative to {@code @RegisterExtension} fields and other
86+
* {@code @ExtendWith} fields.
8587
*
8688
* <h3>Example Usage</h3>
8789
*

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import static java.util.stream.Collectors.joining;
1414
import static org.apiguardian.api.API.Status.INTERNAL;
1515
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.populateNewExtensionRegistryFromExtendWithAnnotation;
16+
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromConstructorParameters;
17+
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromExecutableParameters;
1618
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromFields;
1719
import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.findAfterAllMethods;
1820
import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.findAfterEachMethods;
@@ -152,16 +154,27 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte
152154
// one factory registered per class).
153155
this.testInstanceFactory = resolveTestInstanceFactory(registry);
154156

157+
if (this.testInstanceFactory == null) {
158+
registerExtensionsFromConstructorParameters(registry, this.testClass);
159+
}
160+
161+
this.beforeAllMethods = findBeforeAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD);
162+
this.afterAllMethods = findAfterAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD);
163+
164+
this.beforeAllMethods.forEach(method -> registerExtensionsFromExecutableParameters(registry, method));
165+
// Since registerBeforeEachMethodAdapters() and registerAfterEachMethodAdapters() also
166+
// invoke registerExtensionsFromExecutableParameters(), we invoke those methods before
167+
// invoking registerExtensionsFromExecutableParameters() for @AfterAll methods,
168+
// thereby ensuring proper registration order for extensions registered via @ExtendWith
169+
// on parameters in lifecycle methods.
155170
registerBeforeEachMethodAdapters(registry);
156171
registerAfterEachMethodAdapters(registry);
172+
this.afterAllMethods.forEach(method -> registerExtensionsFromExecutableParameters(registry, method));
157173

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

162-
this.beforeAllMethods = findBeforeAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD);
163-
this.afterAllMethods = findAfterAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD);
164-
165178
// @formatter:off
166179
return context.extend()
167180
.withTestInstancesProvider(testInstancesProvider(context, extensionContext))
@@ -468,7 +481,10 @@ private void registerAfterEachMethodAdapters(ExtensionRegistrar registrar) {
468481
private void registerMethodsAsExtensions(List<Method> methods, ExtensionRegistrar registrar,
469482
Function<Method, Extension> extensionSynthesizer) {
470483

471-
methods.forEach(method -> registrar.registerSyntheticExtension(extensionSynthesizer.apply(method), method));
484+
methods.forEach(method -> {
485+
registerExtensionsFromExecutableParameters(registrar, method);
486+
registrar.registerSyntheticExtension(extensionSynthesizer.apply(method), method);
487+
});
472488
}
473489

474490
private BeforeEachMethodAdapter synthesizeBeforeEachMethodAdapter(Method method) {

0 commit comments

Comments
 (0)