Skip to content

Commit

Permalink
Introduce EnableTestScopedConstructorContext annotation for extensions
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
JojOatXGME and marcphilipp authored Oct 8, 2024
1 parent d274794 commit c2fb8bd
Show file tree
Hide file tree
Showing 22 changed files with 580 additions and 101 deletions.
1 change: 1 addition & 0 deletions documentation/src/docs/asciidoc/link-attributes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ endif::[]
:BeforeAllCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeAllCallback.html[BeforeAllCallback]
:BeforeEachCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeEachCallback.html[BeforeEachCallback]
:BeforeTestExecutionCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeTestExecutionCallback.html[BeforeTestExecutionCallback]
:EnableTestScopedConstructorContext: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/EnableTestScopedConstructorContext.html[@EnableTestScopedConstructorContext]
:ExecutableInvoker: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ExecutableInvoker.html[ExecutableInvoker]
:ExecutionCondition: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ExecutionCondition.html[ExecutionCondition]
:ExtendWith: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ExtendWith.html[@ExtendWith]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ JUnit repository on GitHub.
extensions.
* Allow determining "shared resources" at runtime via the new `@ResourceLock#providers`
attribute that accepts implementations of `ResourceLocksProvider`.
* `@EnableTestScopedConstructorContext` has been added to enable the use of a test-scoped
`ExtensionContext` while instantiating the test instance.
The behavior enabled by the annotation is expected to eventually become the default in
future versions of JUnit Jupiter.


[[release-notes-5.12.0-M1-junit-vintage]]
Expand Down
33 changes: 33 additions & 0 deletions documentation/src/docs/asciidoc/user-guide/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,12 @@ This extension provides a symmetric call to `{TestInstancePreDestroyCallback}` a
in combination with other extensions to prepare constructor parameters or keeping track of test
instances and their lifecycle.

[NOTE]
====
You may annotate your extension with `{EnableTestScopedConstructorContext}` for revised
handling of `CloseableResource` and to make test-specific data available to your implementation.
====

[[extensions-test-instance-factories]]
=== Test Instance Factories

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

[NOTE]
====
You may annotate your extension with `{EnableTestScopedConstructorContext}` for revised
handling of `CloseableResource` and to make test-specific data available to your implementation.
====

[[extensions-test-instance-post-processing]]
=== Test Instance Post-processing

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

[NOTE]
====
You may annotate your extension with `{EnableTestScopedConstructorContext}` for revised
handling of `CloseableResource` and to make test-specific data available to your implementation.
====

[[extensions-test-instance-pre-destroy-callback]]
=== Test Instance Pre-destroy Callback

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

[NOTE]
====
You may annotate your extension with `{EnableTestScopedConstructorContext}` to support
injecting test specific data into constructor parameters of the test instance.
The annotation makes JUnit use a test-specific `ExtensionContext` while resolving
constructor parameters, unless the lifecycle is set to `TestInstance.Lifecycle.PER_CLASS`.
====

[NOTE]
====
Other extensions can also leverage registered `ParameterResolvers` for method and
Expand Down Expand Up @@ -695,6 +721,13 @@ Dispatch Thread.
include::{testDir}/example/interceptor/SwingEdtInterceptor.java[tags=user_guide]
----

[NOTE]
====
You may annotate your extension with `{EnableTestScopedConstructorContext}` to make
test-specific data available to your implementation of `interceptTestClassConstructor` and
for a revised scope of the provided `Store` instance.
====

[[extensions-test-templates]]
=== Providing Invocation Contexts for Test Templates

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.jupiter.api.extension;

import static org.apiguardian.api.API.Status.MAINTAINED;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.apiguardian.api.API;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;

/**
* {@code @EnableTestScopedConstructorContext} allows
* {@link Extension Extensions} to use a test-scoped {@link ExtensionContext}
* during creation of test instances.
*
* <p>The annotation should be used on extension classes.
* JUnit will call the following extension callbacks of annotated extensions
* with a test-scoped {@link ExtensionContext}, unless the test class is
* annotated with {@link TestInstance @TestInstance(Lifecycle.PER_CLASS)}.
*
* <ul>
* <li>{@link InvocationInterceptor#interceptTestClassConstructor(InvocationInterceptor.Invocation, ReflectiveInvocationContext, ExtensionContext) InvocationInterceptor.interceptTestClassConstructor(...)}</li>
* <li>{@link ParameterResolver} when resolving constructor parameters</li>
* <li>{@link TestInstancePreConstructCallback}</li>
* <li>{@link TestInstancePostProcessor}</li>
* <li>{@link TestInstanceFactory}</li>
* </ul>
*
* <p>Implementations of these extension callbacks can observe the following
* differences if they are using {@code @EnableTestScopedConstructorContext}.
*
* <ul>
* <li>{@link ExtensionContext#getElement() getElement()} may refer to the test
* method and {@link ExtensionContext#getTestClass() getTestClass()} may refer
* to a nested test class. Use {@link TestInstanceFactoryContext#getTestClass()}
* to get the class under construction.</li>
* <li>{@link ExtensionContext#getTestMethod() getTestMethod()} is no-longer
* empty, unless the test class is annotated with
* {@link TestInstance @TestInstance(Lifecycle.PER_CLASS)}.</li>
* <li>If the callback adds a new {@link CloseableResource CloseableResource} to
* the {@link Store Store}, the resource is closed just after the instance is
* destroyed.</li>
* <li>The callbacks can now access data previously stored by
* {@link TestTemplateInvocationContext}, unless the test class is annotated
* with {@link TestInstance @TestInstance(Lifecycle.PER_CLASS)}.</li>
* </ul>
*
* <p><strong>Note</strong>: The behavior which is enabled by this annotation is
* expected to become the default in future versions of JUnit Jupiter. To ensure
* future compatibility, extension vendors are therefore advised to annotate
* their extensions, even if they don't need the new functionality.
*
* @since 5.12
* @see InvocationInterceptor
* @see ParameterResolver
* @see TestInstancePreConstructCallback
* @see TestInstancePostProcessor
* @see TestInstanceFactory
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@API(status = MAINTAINED, since = "5.12")
public @interface EnableTestScopedConstructorContext {
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ public interface InvocationInterceptor extends Extension {
* <p>Note that the test class may <em>not</em> have been initialized
* (static initialization) when this method is invoked.
*
* <p>You may annotate your extension with {@link EnableTestScopedConstructorContext}
* to make test-specific data available to your implementation of this method and
* for a revised scope of the provided `Store` instance.
*
* @param invocation the invocation that is being intercepted; never
* {@code null}
* @param invocationContext the context of the invocation that is being
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.lang.reflect.Parameter;

import org.apiguardian.api.API;
import org.junit.jupiter.api.TestInstance;

/**
* {@code ParameterResolver} defines the API for {@link Extension Extensions}
Expand All @@ -30,6 +31,12 @@
* an argument for the parameter must be resolved at runtime by a
* {@code ParameterResolver}.
*
* <p>You may annotate your extension with {@link EnableTestScopedConstructorContext}
* to support injecting test specific data into constructor parameters of the test instance.
* The annotation makes JUnit use a test-specific `ExtensionContext` while resolving
* constructor parameters, unless the test class is annotated with
* {@link TestInstance @TestInstance(Lifecycle.PER_CLASS)}.
*
* <h2>Constructor Requirements</h2>
*
* <p>Consult the documentation in {@link Extension} for details on
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import static org.apiguardian.api.API.Status.STABLE;

import org.apiguardian.api.API;
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;

/**
* {@code TestInstanceFactory} defines the API for {@link Extension
Expand Down Expand Up @@ -56,6 +57,11 @@ public interface TestInstanceFactory extends Extension {
/**
* Callback for creating a test instance for the supplied context.
*
* <p>You may annotate your extension with
* {@link EnableTestScopedConstructorContext @EnableTestScopedConstructorContext}
* for revised handling of {@link CloseableResource CloseableResource} and
* to make test-specific data available to your implementation.
*
* <p><strong>Note</strong>: the {@code ExtensionContext} supplied to a
* {@code TestInstanceFactory} will always return an empty
* {@link java.util.Optional} value from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import static org.apiguardian.api.API.Status.STABLE;

import org.apiguardian.api.API;
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;

/**
* {@code TestInstancePostProcessor} defines the API for {@link Extension
Expand Down Expand Up @@ -45,6 +46,11 @@ public interface TestInstancePostProcessor extends Extension {
/**
* Callback for post-processing the supplied test instance.
*
* <p>You may annotate your extension with
* {@link EnableTestScopedConstructorContext @EnableTestScopedConstructorContext}
* for revised handling of {@link CloseableResource CloseableResource} and
* to make test-specific data available to your implementation.
*
* <p><strong>Note</strong>: the {@code ExtensionContext} supplied to a
* {@code TestInstancePostProcessor} will always return an empty
* {@link java.util.Optional} value from {@link ExtensionContext#getTestInstance()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import org.apiguardian.api.API;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;

/**
* {@code TestInstancePreConstructCallback} defines the API for {@link Extension
Expand Down Expand Up @@ -49,6 +50,11 @@ public interface TestInstancePreConstructCallback extends Extension {
/**
* Callback invoked prior to test instances being constructed.
*
* <p>You may annotate your extension with
* {@link EnableTestScopedConstructorContext @EnableTestScopedConstructorContext}
* for revised handling of {@link CloseableResource CloseableResource} and
* to make test-specific data available to your implementation.
*
* @param factoryContext the context for the test instance about to be instantiated;
* never {@code null}
* @param context the current extension context; never {@code null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import org.junit.jupiter.engine.execution.BeforeEachMethodAdapter;
import org.junit.jupiter.engine.execution.DefaultExecutableInvoker;
import org.junit.jupiter.engine.execution.DefaultTestInstances;
import org.junit.jupiter.engine.execution.ExtensionContextSupplier;
import org.junit.jupiter.engine.execution.InterceptingExecutableInvoker;
import org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.ReflectiveInterceptorCall;
import org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.ReflectiveInterceptorCall.VoidMethodInterceptorCall;
Expand Down Expand Up @@ -202,8 +203,7 @@ public JupiterEngineExecutionContext before(JupiterEngineExecutionContext contex
// and store the instance in the ExtensionContext.
ClassExtensionContext extensionContext = (ClassExtensionContext) context.getExtensionContext();
throwableCollector.execute(() -> {
TestInstances testInstances = context.getTestInstancesProvider().getTestInstances(
context.getExtensionRegistry(), throwableCollector);
TestInstances testInstances = context.getTestInstancesProvider().getTestInstances(context);
extensionContext.setTestInstances(testInstances);
});
}
Expand Down Expand Up @@ -274,35 +274,38 @@ private TestInstanceFactory resolveTestInstanceFactory(ExtensionRegistry registr
}

private TestInstancesProvider testInstancesProvider(JupiterEngineExecutionContext parentExecutionContext,
ClassExtensionContext extensionContext) {
ClassExtensionContext ourExtensionContext) {

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

private TestInstances instantiateAndPostProcessTestInstance(JupiterEngineExecutionContext parentExecutionContext,
ExtensionContext extensionContext, ExtensionRegistry registry, ExtensionRegistrar registrar,
ThrowableCollector throwableCollector) {
ClassExtensionContext ourExtensionContext, ExtensionRegistry registry,
JupiterEngineExecutionContext context) {

TestInstances instances = instantiateTestClass(parentExecutionContext, registry, registrar, extensionContext,
throwableCollector);
throwableCollector.execute(() -> {
ExtensionContextSupplier extensionContext = new ExtensionContextSupplier(context.getExtensionContext(),
ourExtensionContext);
TestInstances instances = instantiateTestClass(parentExecutionContext, extensionContext, registry, context);
context.getThrowableCollector().execute(() -> {
invokeTestInstancePostProcessors(instances.getInnermostInstance(), registry, extensionContext);
// In addition, we initialize extension registered programmatically from instance fields here
// since the best time to do that is immediately following test class instantiation
// and post-processing.
registrar.initializeExtensions(this.testClass, instances.getInnermostInstance());
context.getExtensionRegistry().initializeExtensions(this.testClass, instances.getInnermostInstance());
});
return instances;
}

protected abstract TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext,
ExtensionRegistry registry, ExtensionRegistrar registrar, ExtensionContext extensionContext,
ThrowableCollector throwableCollector);
ExtensionContextSupplier extensionContext, ExtensionRegistry registry,
JupiterEngineExecutionContext context);

protected TestInstances instantiateTestClass(Optional<TestInstances> outerInstances, ExtensionRegistry registry,
ExtensionContext extensionContext) {
ExtensionContextSupplier extensionContext) {

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

private Object invokeTestInstanceFactory(Optional<Object> outerInstance, ExtensionContext extensionContext) {
private Object invokeTestInstanceFactory(Optional<Object> outerInstance,
ExtensionContextSupplier extensionContext) {
Object instance;

try {
ExtensionContext actualExtensionContext = extensionContext.get(this.testInstanceFactory);
instance = this.testInstanceFactory.createTestInstance(
new DefaultTestInstanceFactoryContext(this.testClass, outerInstance), extensionContext);
new DefaultTestInstanceFactoryContext(this.testClass, outerInstance), actualExtensionContext);
}
catch (Throwable throwable) {
UnrecoverableExceptions.rethrowIfUnrecoverable(throwable);
Expand Down Expand Up @@ -359,24 +364,24 @@ private Object invokeTestInstanceFactory(Optional<Object> outerInstance, Extensi
}

private Object invokeTestClassConstructor(Optional<Object> outerInstance, ExtensionRegistry registry,
ExtensionContext extensionContext) {
ExtensionContextSupplier extensionContext) {

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

private void invokeTestInstancePreConstructCallbacks(TestInstanceFactoryContext factoryContext,
ExtensionRegistry registry, ExtensionContext context) {
registry.stream(TestInstancePreConstructCallback.class).forEach(
extension -> executeAndMaskThrowable(() -> extension.preConstructTestInstance(factoryContext, context)));
ExtensionRegistry registry, ExtensionContextSupplier context) {
registry.stream(TestInstancePreConstructCallback.class).forEach(extension -> executeAndMaskThrowable(
() -> extension.preConstructTestInstance(factoryContext, context.get(extension))));
}

private void invokeTestInstancePostProcessors(Object instance, ExtensionRegistry registry,
ExtensionContext context) {
ExtensionContextSupplier context) {

registry.stream(TestInstancePostProcessor.class).forEach(
extension -> executeAndMaskThrowable(() -> extension.postProcessTestInstance(instance, context)));
registry.stream(TestInstancePostProcessor.class).forEach(extension -> executeAndMaskThrowable(
() -> extension.postProcessTestInstance(instance, context.get(extension))));
}

private void executeAndMaskThrowable(Executable executable) {
Expand Down
Loading

0 comments on commit c2fb8bd

Please sign in to comment.