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
@@ -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
diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/EnableTestScopedConstructorContext.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/EnableTestScopedConstructorContext.java
new file mode 100644
index 000000000000..58a3cc65ce5a
--- /dev/null
+++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/EnableTestScopedConstructorContext.java
@@ -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.
+ *
+ * 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)}.
+ *
+ *
+ * - {@link InvocationInterceptor#interceptTestClassConstructor(InvocationInterceptor.Invocation, ReflectiveInvocationContext, ExtensionContext) InvocationInterceptor.interceptTestClassConstructor(...)}
+ * - {@link ParameterResolver} when resolving constructor parameters
+ * - {@link TestInstancePreConstructCallback}
+ * - {@link TestInstancePostProcessor}
+ * - {@link TestInstanceFactory}
+ *
+ *
+ * Implementations of these extension callbacks can observe the following
+ * differences if they are using {@code @EnableTestScopedConstructorContext}.
+ *
+ *
+ * - {@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.
+ * - {@link ExtensionContext#getTestMethod() getTestMethod()} is no-longer
+ * empty, unless the test class is annotated with
+ * {@link TestInstance @TestInstance(Lifecycle.PER_CLASS)}.
+ * - If the callback adds a new {@link CloseableResource CloseableResource} to
+ * the {@link Store Store}, the resource is closed just after the instance is
+ * destroyed.
+ * - The callbacks can now access data previously stored by
+ * {@link TestTemplateInvocationContext}, unless the test class is annotated
+ * with {@link TestInstance @TestInstance(Lifecycle.PER_CLASS)}.
+ *
+ *
+ * Note: 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 {
+}
diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/InvocationInterceptor.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/InvocationInterceptor.java
index 81bf9dc1fd32..c13e6ed841d1 100644
--- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/InvocationInterceptor.java
+++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/InvocationInterceptor.java
@@ -58,6 +58,10 @@ public interface InvocationInterceptor extends Extension {
*
Note that the test class may not have been initialized
* (static initialization) when this method is invoked.
*
+ *
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
diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ParameterResolver.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ParameterResolver.java
index 6678d72b898a..74ad7feaae61 100644
--- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ParameterResolver.java
+++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ParameterResolver.java
@@ -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}
@@ -30,6 +31,12 @@
* an argument for the parameter must be resolved at runtime by a
* {@code ParameterResolver}.
*
+ *
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)}.
+ *
*
Constructor Requirements
*
* Consult the documentation in {@link Extension} for details on
diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstanceFactory.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstanceFactory.java
index a5e7e514c540..f341e88e3e2d 100644
--- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstanceFactory.java
+++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstanceFactory.java
@@ -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
@@ -56,6 +57,11 @@ public interface TestInstanceFactory extends Extension {
/**
* Callback for creating a test instance for the supplied context.
*
+ *
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.
+ *
*
Note: the {@code ExtensionContext} supplied to a
* {@code TestInstanceFactory} will always return an empty
* {@link java.util.Optional} value from
diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstancePostProcessor.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstancePostProcessor.java
index 6b0cd8e59b17..a1aa465c5737 100644
--- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstancePostProcessor.java
+++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstancePostProcessor.java
@@ -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
@@ -45,6 +46,11 @@ public interface TestInstancePostProcessor extends Extension {
/**
* Callback for post-processing the supplied test instance.
*
+ *
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.
+ *
*
Note: the {@code ExtensionContext} supplied to a
* {@code TestInstancePostProcessor} will always return an empty
* {@link java.util.Optional} value from {@link ExtensionContext#getTestInstance()
diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstancePreConstructCallback.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstancePreConstructCallback.java
index 933d7bc9d27b..b627c52f9132 100644
--- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstancePreConstructCallback.java
+++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstancePreConstructCallback.java
@@ -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
@@ -49,6 +50,11 @@ public interface TestInstancePreConstructCallback extends Extension {
/**
* Callback invoked prior to test instances being constructed.
*
+ *
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}
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java
index 6b3b524e6f55..c604cab089cf 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java
@@ -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;
@@ -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);
});
}
@@ -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 outerInstances, ExtensionRegistry registry,
- ExtensionContext extensionContext) {
+ ExtensionContextSupplier extensionContext) {
Optional