Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce EnableTestScopedConstructorContext annotation for extensions #4032

Merged
merged 4 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,7 @@ JUnit repository on GitHub.
[[release-notes-5.12.0-M1-junit-jupiter-deprecations-and-breaking-changes]]
==== Deprecations and Breaking Changes

* `ParameterResolver` extensions receive a different `ExtensionContext` for constructor
parameters of the test instance. Since the `ExtensionContext` is now consistent with
parameters of test methods, extensions are unlikely to break, but the behavior may
change in certain scenarios.
* ❓

[[release-notes-5.12.0-M1-junit-jupiter-new-features-and-improvements]]
==== New Features and Improvements
Expand All @@ -57,12 +54,10 @@ JUnit repository on GitHub.
`@ConvertWith`), and `ArgumentsAggregator` (declared via `@AggregateWith`)
implementations can now use constructor injection from registered `ParameterResolver`
extensions.
* Implementations of `ParameterResolver` now receive a test-specific `ExtensionContext`
for constructor parameters of the test class.
* `@EnableTestScopedConstructorContext` has been added to enable the use of a test-scoped
`ExtensionContext` in `TestInstancePreConstructCallback`, `TestInstancePostProcessor`
and `TestInstanceFactory`. The behavior enabled by the annotation is expected to
eventually become the default in future versions of JUnit Jupiter.
`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
15 changes: 15 additions & 0 deletions documentation/src/docs/asciidoc/user-guide/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -483,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 @@ -713,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
3 changes: 1 addition & 2 deletions documentation/src/test/java/example/TestInfoDemo.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@
class TestInfoDemo {

TestInfoDemo(TestInfo testInfo) {
String displayName = testInfo.getDisplayName();
assertTrue(displayName.equals("TEST 1") || displayName.equals("test2()"));
assertEquals("TestInfo Demo", testInfo.getDisplayName());
marcphilipp marked this conversation as resolved.
Show resolved Hide resolved
}

@BeforeEach
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
* 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>
Expand Down Expand Up @@ -64,6 +66,8 @@
* their extensions, even if they don't need the new functionality.
*
* @since 5.12
* @see InvocationInterceptor
* @see ParameterResolver
* @see TestInstancePreConstructCallback
* @see TestInstancePostProcessor
* @see TestInstanceFactory
Expand Down
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 @@ -39,7 +39,6 @@
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.EnableTestScopedConstructorContext;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junit.jupiter.api.extension.ExtensionContext;
Expand All @@ -58,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 All @@ -67,7 +67,6 @@
import org.junit.jupiter.engine.extension.ExtensionRegistry;
import org.junit.jupiter.engine.extension.MutableExtensionRegistry;
import org.junit.platform.commons.JUnitException;
import org.junit.platform.commons.util.AnnotationUtils;
import org.junit.platform.commons.util.ExceptionUtils;
import org.junit.platform.commons.util.ReflectionUtils;
import org.junit.platform.commons.util.StringUtils;
Expand Down Expand Up @@ -287,10 +286,11 @@ private TestInstances instantiateAndPostProcessTestInstance(JupiterEngineExecuti
ClassExtensionContext ourExtensionContext, ExtensionRegistry registry,
JupiterEngineExecutionContext context) {

TestInstances instances = instantiateTestClass(parentExecutionContext, ourExtensionContext, registry, context);
ExtensionContextSupplier extensionContext = new ExtensionContextSupplier(context.getExtensionContext(),
ourExtensionContext);
TestInstances instances = instantiateTestClass(parentExecutionContext, extensionContext, registry, context);
context.getThrowableCollector().execute(() -> {
invokeTestInstancePostProcessors(instances.getInnermostInstance(), registry, context.getExtensionContext(),
ourExtensionContext);
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.
Expand All @@ -300,35 +300,30 @@ private TestInstances instantiateAndPostProcessTestInstance(JupiterEngineExecuti
}

protected abstract TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext,
ExtensionContext ourExtensionContext, ExtensionRegistry registry, JupiterEngineExecutionContext context);
ExtensionContextSupplier extensionContext, ExtensionRegistry registry,
JupiterEngineExecutionContext context);

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

Optional<Object> outerInstance = outerInstances.map(TestInstances::getInnermostInstance);
invokeTestInstancePreConstructCallbacks(new DefaultTestInstanceFactoryContext(this.testClass, outerInstance),
registry, extensionContext, ourExtensionContext);
registry, extensionContext);
Object instance = this.testInstanceFactory != null //
? invokeTestInstanceFactory(outerInstance, extensionContext, ourExtensionContext) //
: invokeTestClassConstructor(outerInstance, registry, extensionContext, ourExtensionContext);
? invokeTestInstanceFactory(outerInstance, extensionContext) //
: invokeTestClassConstructor(outerInstance, registry, extensionContext);
return outerInstances.map(instances -> DefaultTestInstances.of(instances, instance)).orElse(
DefaultTestInstances.of(instance));
}

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

try {
if (AnnotationUtils.isAnnotated(this.testInstanceFactory.getClass(),
EnableTestScopedConstructorContext.class)) {
instance = this.testInstanceFactory.createTestInstance(
new DefaultTestInstanceFactoryContext(this.testClass, outerInstance), extensionContext);
}
else {
instance = this.testInstanceFactory.createTestInstance(
new DefaultTestInstanceFactoryContext(this.testClass, outerInstance), ourExtensionContext);
}
ExtensionContext actualExtensionContext = extensionContext.get(this.testInstanceFactory);
instance = this.testInstanceFactory.createTestInstance(
new DefaultTestInstanceFactoryContext(this.testClass, outerInstance), actualExtensionContext);
}
catch (Throwable throwable) {
UnrecoverableExceptions.rethrowIfUnrecoverable(throwable);
Expand Down Expand Up @@ -368,36 +363,24 @@ private Object invokeTestInstanceFactory(Optional<Object> outerInstance, Extensi
}

private Object invokeTestClassConstructor(Optional<Object> outerInstance, ExtensionRegistry registry,
ExtensionContext extensionContext, ExtensionContext ourExtensionContext) {
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, ExtensionContext ourContext) {
registry.stream(TestInstancePreConstructCallback.class).forEach(extension -> {
if (AnnotationUtils.isAnnotated(extension.getClass(), EnableTestScopedConstructorContext.class)) {
executeAndMaskThrowable(() -> extension.preConstructTestInstance(factoryContext, context));
}
else {
executeAndMaskThrowable(() -> extension.preConstructTestInstance(factoryContext, ourContext));
}
});
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,
ClassExtensionContext ourContext) {
private void invokeTestInstancePostProcessors(Object instance, ExtensionRegistry registry,
ExtensionContextSupplier context) {

registry.stream(TestInstancePostProcessor.class).forEach(extension -> {
if (AnnotationUtils.isAnnotated(extension.getClass(), EnableTestScopedConstructorContext.class)) {
executeAndMaskThrowable(() -> extension.postProcessTestInstance(instance, context));
}
else {
executeAndMaskThrowable(() -> extension.postProcessTestInstance(instance, ourContext));
}
});
registry.stream(TestInstancePostProcessor.class).forEach(extension -> executeAndMaskThrowable(
() -> extension.postProcessTestInstance(instance, context.get(extension))));
}

private void executeAndMaskThrowable(Executable executable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
import java.util.Set;

import org.apiguardian.api.API;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestInstances;
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.jupiter.engine.execution.ExtensionContextSupplier;
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
import org.junit.jupiter.engine.extension.ExtensionRegistry;
import org.junit.platform.engine.TestDescriptor;
Expand Down Expand Up @@ -72,8 +72,9 @@ public ExecutionMode getExecutionMode() {

@Override
protected TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext,
ExtensionContext ourExtensionContext, ExtensionRegistry registry, JupiterEngineExecutionContext context) {
return instantiateTestClass(Optional.empty(), registry, context.getExtensionContext(), ourExtensionContext);
ExtensionContextSupplier extensionContext, ExtensionRegistry registry,
JupiterEngineExecutionContext context) {
return instantiateTestClass(Optional.empty(), registry, extensionContext);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
import java.util.Set;

import org.apiguardian.api.API;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestInstances;
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.jupiter.engine.execution.ExtensionContextSupplier;
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
import org.junit.jupiter.engine.extension.ExtensionRegistry;
import org.junit.platform.engine.TestDescriptor;
Expand Down Expand Up @@ -75,14 +75,14 @@ public List<Class<?>> getEnclosingTestClasses() {

@Override
protected TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext,
ExtensionContext ourExtensionContext, ExtensionRegistry registry, JupiterEngineExecutionContext context) {
ExtensionContextSupplier extensionContext, ExtensionRegistry registry,
JupiterEngineExecutionContext context) {

// Extensions registered for nested classes and below are not to be used for instantiating and initializing outer classes
ExtensionRegistry extensionRegistryForOuterInstanceCreation = parentExecutionContext.getExtensionRegistry();
TestInstances outerInstances = parentExecutionContext.getTestInstancesProvider().getTestInstances(
extensionRegistryForOuterInstanceCreation, context);
return instantiateTestClass(Optional.of(outerInstances), registry, context.getExtensionContext(),
ourExtensionContext);
return instantiateTestClass(Optional.of(outerInstances), registry, extensionContext);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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.engine.execution;

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

import org.apiguardian.api.API;
import org.junit.jupiter.api.extension.EnableTestScopedConstructorContext;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.platform.commons.util.AnnotationUtils;

/**
* Container of two instances of {@link ExtensionContext} to simplify the legacy for
* <a href="https://github.com/junit-team/junit5/issues/3445">#3445 (Introduction of Test-scoped ExtensionContext)</a>.
*
* @since 5.12
*/
@API(status = INTERNAL, since = "5.12")
public final class ExtensionContextSupplier {

private final ExtensionContext currentExtensionContext;
private final ExtensionContext legacyExtensionContext;

public ExtensionContextSupplier(ExtensionContext currentExtensionContext, ExtensionContext legacyExtensionContext) {
this.currentExtensionContext = currentExtensionContext;
this.legacyExtensionContext = legacyExtensionContext;
}

public ExtensionContext get(Extension extension) {
if (currentExtensionContext == legacyExtensionContext
|| AnnotationUtils.isAnnotated(extension.getClass(), EnableTestScopedConstructorContext.class)) {
return currentExtensionContext;
}
else {
return legacyExtensionContext;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ public class InterceptingExecutableInvoker {
* invocation via all registered {@linkplain InvocationInterceptor
* interceptors}
*/
public <T> T invoke(Constructor<T> constructor, Optional<Object> outerInstance, ExtensionContext extensionContext,
ExtensionRegistry extensionRegistry, ReflectiveInterceptorCall<Constructor<T>, T> interceptorCall) {
public <T> T invoke(Constructor<T> constructor, Optional<Object> outerInstance,
ExtensionContextSupplier extensionContext, ExtensionRegistry extensionRegistry,
ReflectiveInterceptorCall<Constructor<T>, T> interceptorCall) {

Object[] arguments = resolveParameters(constructor, Optional.empty(), outerInstance, extensionContext,
extensionRegistry);
Expand Down Expand Up @@ -93,6 +94,14 @@ private <E extends Executable, T> T invoke(Invocation<T> originalInvocation,
wrappedInvocation) -> call.apply(interceptor, wrappedInvocation, invocationContext, extensionContext));
}

private <E extends Executable, T> T invoke(Invocation<T> originalInvocation,
ReflectiveInvocationContext<E> invocationContext, ExtensionContextSupplier extensionContext,
ExtensionRegistry extensionRegistry, ReflectiveInterceptorCall<E, T> call) {
return interceptorChain.invoke(originalInvocation, extensionRegistry,
(interceptor, wrappedInvocation) -> call.apply(interceptor, wrappedInvocation, invocationContext,
extensionContext.get(interceptor)));
}

public interface ReflectiveInterceptorCall<E extends Executable, T> {

T apply(InvocationInterceptor interceptor, Invocation<T> invocation,
Expand Down
Loading