Skip to content

Commit c2fb8bd

Browse files
Introduce EnableTestScopedConstructorContext annotation for extensions
The new annotation allows extensions to opt in to receive a test-scoped `ExtensionContext` for extension methods participating in the creation or destruction of test class instances: - `TestInstancePreConstructCallback` - `TestInstanceFactory` - `ParameterResolver` (when called for a test class constructor) - `InvocationInterceptor.interceptTestClassConstructor` - `TestInstancePostProcessor` Resolves #3445. --------- Co-authored-by: Marc Philipp <[email protected]>
1 parent d274794 commit c2fb8bd

22 files changed

+580
-101
lines changed

documentation/src/docs/asciidoc/link-attributes.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ endif::[]
139139
:BeforeAllCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeAllCallback.html[BeforeAllCallback]
140140
:BeforeEachCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeEachCallback.html[BeforeEachCallback]
141141
:BeforeTestExecutionCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeTestExecutionCallback.html[BeforeTestExecutionCallback]
142+
:EnableTestScopedConstructorContext: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/EnableTestScopedConstructorContext.html[@EnableTestScopedConstructorContext]
142143
:ExecutableInvoker: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ExecutableInvoker.html[ExecutableInvoker]
143144
:ExecutionCondition: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ExecutionCondition.html[ExecutionCondition]
144145
:ExtendWith: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ExtendWith.html[@ExtendWith]

documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ JUnit repository on GitHub.
5858
extensions.
5959
* Allow determining "shared resources" at runtime via the new `@ResourceLock#providers`
6060
attribute that accepts implementations of `ResourceLocksProvider`.
61+
* `@EnableTestScopedConstructorContext` has been added to enable the use of a test-scoped
62+
`ExtensionContext` while instantiating the test instance.
63+
The behavior enabled by the annotation is expected to eventually become the default in
64+
future versions of JUnit Jupiter.
6165

6266

6367
[[release-notes-5.12.0-M1-junit-vintage]]

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,12 @@ This extension provides a symmetric call to `{TestInstancePreDestroyCallback}` a
381381
in combination with other extensions to prepare constructor parameters or keeping track of test
382382
instances and their lifecycle.
383383

384+
[NOTE]
385+
====
386+
You may annotate your extension with `{EnableTestScopedConstructorContext}` for revised
387+
handling of `CloseableResource` and to make test-specific data available to your implementation.
388+
====
389+
384390
[[extensions-test-instance-factories]]
385391
=== Test Instance Factories
386392

@@ -407,6 +413,12 @@ the user's responsibility to ensure that only a single `TestInstanceFactory` is
407413
registered for any specific test class.
408414
====
409415

416+
[NOTE]
417+
====
418+
You may annotate your extension with `{EnableTestScopedConstructorContext}` for revised
419+
handling of `CloseableResource` and to make test-specific data available to your implementation.
420+
====
421+
410422
[[extensions-test-instance-post-processing]]
411423
=== Test Instance Post-processing
412424

@@ -419,6 +431,12 @@ initialization methods on the test instance, etc.
419431
For a concrete example, consult the source code for the `{MockitoExtension}` and the
420432
`{SpringExtension}`.
421433

434+
[NOTE]
435+
====
436+
You may annotate your extension with `{EnableTestScopedConstructorContext}` for revised
437+
handling of `CloseableResource` and to make test-specific data available to your implementation.
438+
====
439+
422440
[[extensions-test-instance-pre-destroy-callback]]
423441
=== Test Instance Pre-destroy Callback
424442

@@ -465,6 +483,14 @@ those provided in `java.lang.reflect.Parameter` in order to avoid this bug in th
465483
* `List<A> findRepeatableAnnotations(Class<A> annotationType)`
466484
====
467485

486+
[NOTE]
487+
====
488+
You may annotate your extension with `{EnableTestScopedConstructorContext}` to support
489+
injecting test specific data into constructor parameters of the test instance.
490+
The annotation makes JUnit use a test-specific `ExtensionContext` while resolving
491+
constructor parameters, unless the lifecycle is set to `TestInstance.Lifecycle.PER_CLASS`.
492+
====
493+
468494
[NOTE]
469495
====
470496
Other extensions can also leverage registered `ParameterResolvers` for method and
@@ -695,6 +721,13 @@ Dispatch Thread.
695721
include::{testDir}/example/interceptor/SwingEdtInterceptor.java[tags=user_guide]
696722
----
697723

724+
[NOTE]
725+
====
726+
You may annotate your extension with `{EnableTestScopedConstructorContext}` to make
727+
test-specific data available to your implementation of `interceptTestClassConstructor` and
728+
for a revised scope of the provided `Store` instance.
729+
====
730+
698731
[[extensions-test-templates]]
699732
=== Providing Invocation Contexts for Test Templates
700733

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2015-2024 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.jupiter.api.extension;
12+
13+
import static org.apiguardian.api.API.Status.MAINTAINED;
14+
15+
import java.lang.annotation.ElementType;
16+
import java.lang.annotation.Inherited;
17+
import java.lang.annotation.Retention;
18+
import java.lang.annotation.RetentionPolicy;
19+
import java.lang.annotation.Target;
20+
21+
import org.apiguardian.api.API;
22+
import org.junit.jupiter.api.TestInstance;
23+
import org.junit.jupiter.api.extension.ExtensionContext.Store;
24+
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;
25+
26+
/**
27+
* {@code @EnableTestScopedConstructorContext} allows
28+
* {@link Extension Extensions} to use a test-scoped {@link ExtensionContext}
29+
* during creation of test instances.
30+
*
31+
* <p>The annotation should be used on extension classes.
32+
* JUnit will call the following extension callbacks of annotated extensions
33+
* with a test-scoped {@link ExtensionContext}, unless the test class is
34+
* annotated with {@link TestInstance @TestInstance(Lifecycle.PER_CLASS)}.
35+
*
36+
* <ul>
37+
* <li>{@link InvocationInterceptor#interceptTestClassConstructor(InvocationInterceptor.Invocation, ReflectiveInvocationContext, ExtensionContext) InvocationInterceptor.interceptTestClassConstructor(...)}</li>
38+
* <li>{@link ParameterResolver} when resolving constructor parameters</li>
39+
* <li>{@link TestInstancePreConstructCallback}</li>
40+
* <li>{@link TestInstancePostProcessor}</li>
41+
* <li>{@link TestInstanceFactory}</li>
42+
* </ul>
43+
*
44+
* <p>Implementations of these extension callbacks can observe the following
45+
* differences if they are using {@code @EnableTestScopedConstructorContext}.
46+
*
47+
* <ul>
48+
* <li>{@link ExtensionContext#getElement() getElement()} may refer to the test
49+
* method and {@link ExtensionContext#getTestClass() getTestClass()} may refer
50+
* to a nested test class. Use {@link TestInstanceFactoryContext#getTestClass()}
51+
* to get the class under construction.</li>
52+
* <li>{@link ExtensionContext#getTestMethod() getTestMethod()} is no-longer
53+
* empty, unless the test class is annotated with
54+
* {@link TestInstance @TestInstance(Lifecycle.PER_CLASS)}.</li>
55+
* <li>If the callback adds a new {@link CloseableResource CloseableResource} to
56+
* the {@link Store Store}, the resource is closed just after the instance is
57+
* destroyed.</li>
58+
* <li>The callbacks can now access data previously stored by
59+
* {@link TestTemplateInvocationContext}, unless the test class is annotated
60+
* with {@link TestInstance @TestInstance(Lifecycle.PER_CLASS)}.</li>
61+
* </ul>
62+
*
63+
* <p><strong>Note</strong>: The behavior which is enabled by this annotation is
64+
* expected to become the default in future versions of JUnit Jupiter. To ensure
65+
* future compatibility, extension vendors are therefore advised to annotate
66+
* their extensions, even if they don't need the new functionality.
67+
*
68+
* @since 5.12
69+
* @see InvocationInterceptor
70+
* @see ParameterResolver
71+
* @see TestInstancePreConstructCallback
72+
* @see TestInstancePostProcessor
73+
* @see TestInstanceFactory
74+
*/
75+
@Target(ElementType.TYPE)
76+
@Retention(RetentionPolicy.RUNTIME)
77+
@Inherited
78+
@API(status = MAINTAINED, since = "5.12")
79+
public @interface EnableTestScopedConstructorContext {
80+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ public interface InvocationInterceptor extends Extension {
5858
* <p>Note that the test class may <em>not</em> have been initialized
5959
* (static initialization) when this method is invoked.
6060
*
61+
* <p>You may annotate your extension with {@link EnableTestScopedConstructorContext}
62+
* to make test-specific data available to your implementation of this method and
63+
* for a revised scope of the provided `Store` instance.
64+
*
6165
* @param invocation the invocation that is being intercepted; never
6266
* {@code null}
6367
* @param invocationContext the context of the invocation that is being

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.lang.reflect.Parameter;
1616

1717
import org.apiguardian.api.API;
18+
import org.junit.jupiter.api.TestInstance;
1819

1920
/**
2021
* {@code ParameterResolver} defines the API for {@link Extension Extensions}
@@ -30,6 +31,12 @@
3031
* an argument for the parameter must be resolved at runtime by a
3132
* {@code ParameterResolver}.
3233
*
34+
* <p>You may annotate your extension with {@link EnableTestScopedConstructorContext}
35+
* to support injecting test specific data into constructor parameters of the test instance.
36+
* The annotation makes JUnit use a test-specific `ExtensionContext` while resolving
37+
* constructor parameters, unless the test class is annotated with
38+
* {@link TestInstance @TestInstance(Lifecycle.PER_CLASS)}.
39+
*
3340
* <h2>Constructor Requirements</h2>
3441
*
3542
* <p>Consult the documentation in {@link Extension} for details on

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import static org.apiguardian.api.API.Status.STABLE;
1414

1515
import org.apiguardian.api.API;
16+
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;
1617

1718
/**
1819
* {@code TestInstanceFactory} defines the API for {@link Extension
@@ -56,6 +57,11 @@ public interface TestInstanceFactory extends Extension {
5657
/**
5758
* Callback for creating a test instance for the supplied context.
5859
*
60+
* <p>You may annotate your extension with
61+
* {@link EnableTestScopedConstructorContext @EnableTestScopedConstructorContext}
62+
* for revised handling of {@link CloseableResource CloseableResource} and
63+
* to make test-specific data available to your implementation.
64+
*
5965
* <p><strong>Note</strong>: the {@code ExtensionContext} supplied to a
6066
* {@code TestInstanceFactory} will always return an empty
6167
* {@link java.util.Optional} value from

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import static org.apiguardian.api.API.Status.STABLE;
1414

1515
import org.apiguardian.api.API;
16+
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;
1617

1718
/**
1819
* {@code TestInstancePostProcessor} defines the API for {@link Extension
@@ -45,6 +46,11 @@ public interface TestInstancePostProcessor extends Extension {
4546
/**
4647
* Callback for post-processing the supplied test instance.
4748
*
49+
* <p>You may annotate your extension with
50+
* {@link EnableTestScopedConstructorContext @EnableTestScopedConstructorContext}
51+
* for revised handling of {@link CloseableResource CloseableResource} and
52+
* to make test-specific data available to your implementation.
53+
*
4854
* <p><strong>Note</strong>: the {@code ExtensionContext} supplied to a
4955
* {@code TestInstancePostProcessor} will always return an empty
5056
* {@link java.util.Optional} value from {@link ExtensionContext#getTestInstance()

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import org.apiguardian.api.API;
1616
import org.junit.jupiter.api.TestInstance.Lifecycle;
17+
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;
1718

1819
/**
1920
* {@code TestInstancePreConstructCallback} defines the API for {@link Extension
@@ -49,6 +50,11 @@ public interface TestInstancePreConstructCallback extends Extension {
4950
/**
5051
* Callback invoked prior to test instances being constructed.
5152
*
53+
* <p>You may annotate your extension with
54+
* {@link EnableTestScopedConstructorContext @EnableTestScopedConstructorContext}
55+
* for revised handling of {@link CloseableResource CloseableResource} and
56+
* to make test-specific data available to your implementation.
57+
*
5258
* @param factoryContext the context for the test instance about to be instantiated;
5359
* never {@code null}
5460
* @param context the current extension context; never {@code null}

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

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import org.junit.jupiter.engine.execution.BeforeEachMethodAdapter;
5858
import org.junit.jupiter.engine.execution.DefaultExecutableInvoker;
5959
import org.junit.jupiter.engine.execution.DefaultTestInstances;
60+
import org.junit.jupiter.engine.execution.ExtensionContextSupplier;
6061
import org.junit.jupiter.engine.execution.InterceptingExecutableInvoker;
6162
import org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.ReflectiveInterceptorCall;
6263
import org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.ReflectiveInterceptorCall.VoidMethodInterceptorCall;
@@ -202,8 +203,7 @@ public JupiterEngineExecutionContext before(JupiterEngineExecutionContext contex
202203
// and store the instance in the ExtensionContext.
203204
ClassExtensionContext extensionContext = (ClassExtensionContext) context.getExtensionContext();
204205
throwableCollector.execute(() -> {
205-
TestInstances testInstances = context.getTestInstancesProvider().getTestInstances(
206-
context.getExtensionRegistry(), throwableCollector);
206+
TestInstances testInstances = context.getTestInstancesProvider().getTestInstances(context);
207207
extensionContext.setTestInstances(testInstances);
208208
});
209209
}
@@ -274,35 +274,38 @@ private TestInstanceFactory resolveTestInstanceFactory(ExtensionRegistry registr
274274
}
275275

276276
private TestInstancesProvider testInstancesProvider(JupiterEngineExecutionContext parentExecutionContext,
277-
ClassExtensionContext extensionContext) {
277+
ClassExtensionContext ourExtensionContext) {
278278

279-
return (registry, registrar, throwableCollector) -> extensionContext.getTestInstances().orElseGet(
280-
() -> instantiateAndPostProcessTestInstance(parentExecutionContext, extensionContext, registry, registrar,
281-
throwableCollector));
279+
// For Lifecycle.PER_CLASS, ourExtensionContext.getTestInstances() is used to store the instance.
280+
// Otherwise, extensionContext.getTestInstances() is always empty and we always create a new instance.
281+
return (registry, context) -> ourExtensionContext.getTestInstances().orElseGet(
282+
() -> instantiateAndPostProcessTestInstance(parentExecutionContext, ourExtensionContext, registry,
283+
context));
282284
}
283285

284286
private TestInstances instantiateAndPostProcessTestInstance(JupiterEngineExecutionContext parentExecutionContext,
285-
ExtensionContext extensionContext, ExtensionRegistry registry, ExtensionRegistrar registrar,
286-
ThrowableCollector throwableCollector) {
287+
ClassExtensionContext ourExtensionContext, ExtensionRegistry registry,
288+
JupiterEngineExecutionContext context) {
287289

288-
TestInstances instances = instantiateTestClass(parentExecutionContext, registry, registrar, extensionContext,
289-
throwableCollector);
290-
throwableCollector.execute(() -> {
290+
ExtensionContextSupplier extensionContext = new ExtensionContextSupplier(context.getExtensionContext(),
291+
ourExtensionContext);
292+
TestInstances instances = instantiateTestClass(parentExecutionContext, extensionContext, registry, context);
293+
context.getThrowableCollector().execute(() -> {
291294
invokeTestInstancePostProcessors(instances.getInnermostInstance(), registry, extensionContext);
292295
// In addition, we initialize extension registered programmatically from instance fields here
293296
// since the best time to do that is immediately following test class instantiation
294297
// and post-processing.
295-
registrar.initializeExtensions(this.testClass, instances.getInnermostInstance());
298+
context.getExtensionRegistry().initializeExtensions(this.testClass, instances.getInnermostInstance());
296299
});
297300
return instances;
298301
}
299302

300303
protected abstract TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext,
301-
ExtensionRegistry registry, ExtensionRegistrar registrar, ExtensionContext extensionContext,
302-
ThrowableCollector throwableCollector);
304+
ExtensionContextSupplier extensionContext, ExtensionRegistry registry,
305+
JupiterEngineExecutionContext context);
303306

304307
protected TestInstances instantiateTestClass(Optional<TestInstances> outerInstances, ExtensionRegistry registry,
305-
ExtensionContext extensionContext) {
308+
ExtensionContextSupplier extensionContext) {
306309

307310
Optional<Object> outerInstance = outerInstances.map(TestInstances::getInnermostInstance);
308311
invokeTestInstancePreConstructCallbacks(new DefaultTestInstanceFactoryContext(this.testClass, outerInstance),
@@ -314,12 +317,14 @@ protected TestInstances instantiateTestClass(Optional<TestInstances> outerInstan
314317
DefaultTestInstances.of(instance));
315318
}
316319

317-
private Object invokeTestInstanceFactory(Optional<Object> outerInstance, ExtensionContext extensionContext) {
320+
private Object invokeTestInstanceFactory(Optional<Object> outerInstance,
321+
ExtensionContextSupplier extensionContext) {
318322
Object instance;
319323

320324
try {
325+
ExtensionContext actualExtensionContext = extensionContext.get(this.testInstanceFactory);
321326
instance = this.testInstanceFactory.createTestInstance(
322-
new DefaultTestInstanceFactoryContext(this.testClass, outerInstance), extensionContext);
327+
new DefaultTestInstanceFactoryContext(this.testClass, outerInstance), actualExtensionContext);
323328
}
324329
catch (Throwable throwable) {
325330
UnrecoverableExceptions.rethrowIfUnrecoverable(throwable);
@@ -359,24 +364,24 @@ private Object invokeTestInstanceFactory(Optional<Object> outerInstance, Extensi
359364
}
360365

361366
private Object invokeTestClassConstructor(Optional<Object> outerInstance, ExtensionRegistry registry,
362-
ExtensionContext extensionContext) {
367+
ExtensionContextSupplier extensionContext) {
363368

364369
Constructor<?> constructor = ReflectionUtils.getDeclaredConstructor(this.testClass);
365370
return executableInvoker.invoke(constructor, outerInstance, extensionContext, registry,
366371
InvocationInterceptor::interceptTestClassConstructor);
367372
}
368373

369374
private void invokeTestInstancePreConstructCallbacks(TestInstanceFactoryContext factoryContext,
370-
ExtensionRegistry registry, ExtensionContext context) {
371-
registry.stream(TestInstancePreConstructCallback.class).forEach(
372-
extension -> executeAndMaskThrowable(() -> extension.preConstructTestInstance(factoryContext, context)));
375+
ExtensionRegistry registry, ExtensionContextSupplier context) {
376+
registry.stream(TestInstancePreConstructCallback.class).forEach(extension -> executeAndMaskThrowable(
377+
() -> extension.preConstructTestInstance(factoryContext, context.get(extension))));
373378
}
374379

375380
private void invokeTestInstancePostProcessors(Object instance, ExtensionRegistry registry,
376-
ExtensionContext context) {
381+
ExtensionContextSupplier context) {
377382

378-
registry.stream(TestInstancePostProcessor.class).forEach(
379-
extension -> executeAndMaskThrowable(() -> extension.postProcessTestInstance(instance, context)));
383+
registry.stream(TestInstancePostProcessor.class).forEach(extension -> executeAndMaskThrowable(
384+
() -> extension.postProcessTestInstance(instance, context.get(extension))));
380385
}
381386

382387
private void executeAndMaskThrowable(Executable executable) {

0 commit comments

Comments
 (0)