From c2fb8bd7a57911470d458c6854e721d759cdd647 Mon Sep 17 00:00:00 2001 From: Johannes Spangenberg Date: Tue, 8 Oct 2024 12:29:47 +0200 Subject: [PATCH] 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 --- .../src/docs/asciidoc/link-attributes.adoc | 1 + .../release-notes-5.12.0-M1.adoc | 4 + .../docs/asciidoc/user-guide/extensions.adoc | 33 ++++ .../EnableTestScopedConstructorContext.java | 80 +++++++++ .../api/extension/InvocationInterceptor.java | 4 + .../api/extension/ParameterResolver.java | 7 + .../api/extension/TestInstanceFactory.java | 6 + .../extension/TestInstancePostProcessor.java | 6 + .../TestInstancePreConstructCallback.java | 6 + .../descriptor/ClassBasedTestDescriptor.java | 53 +++--- .../descriptor/ClassTestDescriptor.java | 8 +- .../descriptor/NestedClassTestDescriptor.java | 10 +- .../descriptor/TestMethodTestDescriptor.java | 13 +- .../execution/ExtensionContextSupplier.java | 47 +++++ .../InterceptingExecutableInvoker.java | 13 +- .../execution/ParameterResolutionUtils.java | 13 +- .../execution/TestInstancesProvider.java | 11 +- .../engine/TestInstanceLifecycleTests.java | 3 +- .../InterceptingExecutableInvokerTests.java | 3 +- .../extension/TestInstanceFactoryTests.java | 80 ++++++++- .../TestInstancePostProcessorTests.java | 120 +++++++++---- ...TestInstancePreConstructCallbackTests.java | 160 +++++++++++++++++- 22 files changed, 580 insertions(+), 101 deletions(-) create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/EnableTestScopedConstructorContext.java create mode 100644 junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ExtensionContextSupplier.java diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc index 5c55bec09cf7..90a80a566f55 100644 --- a/documentation/src/docs/asciidoc/link-attributes.adoc +++ b/documentation/src/docs/asciidoc/link-attributes.adoc @@ -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] diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc index 252a95e7b4ee..67393a8cd47b 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc @@ -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]] diff --git a/documentation/src/docs/asciidoc/user-guide/extensions.adoc b/documentation/src/docs/asciidoc/user-guide/extensions.adoc index bffbd523d5ab..12f8b6ad54ad 100644 --- a/documentation/src/docs/asciidoc/user-guide/extensions.adoc +++ b/documentation/src/docs/asciidoc/user-guide/extensions.adoc @@ -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 @@ -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 @@ -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 @@ -465,6 +483,14 @@ those provided in `java.lang.reflect.Parameter` in order to avoid this bug in th * `List findRepeatableAnnotations(Class 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 outerInstance = outerInstances.map(TestInstances::getInnermostInstance); invokeTestInstancePreConstructCallbacks(new DefaultTestInstanceFactoryContext(this.testClass, outerInstance), @@ -314,12 +317,14 @@ protected TestInstances instantiateTestClass(Optional outerInstan DefaultTestInstances.of(instance)); } - private Object invokeTestInstanceFactory(Optional outerInstance, ExtensionContext extensionContext) { + private Object invokeTestInstanceFactory(Optional 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); @@ -359,7 +364,7 @@ private Object invokeTestInstanceFactory(Optional outerInstance, Extensi } private Object invokeTestClassConstructor(Optional outerInstance, ExtensionRegistry registry, - ExtensionContext extensionContext) { + ExtensionContextSupplier extensionContext) { Constructor constructor = ReflectionUtils.getDeclaredConstructor(this.testClass); return executableInvoker.invoke(constructor, outerInstance, extensionContext, registry, @@ -367,16 +372,16 @@ private Object invokeTestClassConstructor(Optional outerInstance, Extens } 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) { diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java index e644518fb57b..3b4b1db2a1de 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java @@ -20,17 +20,15 @@ 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.api.parallel.ResourceLocksProvider; 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.ExtensionRegistrar; import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestTag; import org.junit.platform.engine.UniqueId; -import org.junit.platform.engine.support.hierarchical.ThrowableCollector; /** * {@link TestDescriptor} for tests based on Java classes. @@ -75,8 +73,8 @@ public ExecutionMode getExecutionMode() { @Override protected TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext, - ExtensionRegistry registry, ExtensionRegistrar registrar, ExtensionContext extensionContext, - ThrowableCollector throwableCollector) { + ExtensionContextSupplier extensionContext, ExtensionRegistry registry, + JupiterEngineExecutionContext context) { return instantiateTestClass(Optional.empty(), registry, extensionContext); } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/NestedClassTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/NestedClassTestDescriptor.java index 0c1f697b2686..f8ddd867239a 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/NestedClassTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/NestedClassTestDescriptor.java @@ -21,17 +21,15 @@ 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.api.parallel.ResourceLocksProvider; 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.ExtensionRegistrar; import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestTag; import org.junit.platform.engine.UniqueId; -import org.junit.platform.engine.support.hierarchical.ThrowableCollector; /** * {@link TestDescriptor} for tests based on nested (but not static) Java classes. @@ -78,13 +76,13 @@ public List> getEnclosingTestClasses() { @Override protected TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext, - ExtensionRegistry registry, ExtensionRegistrar registrar, ExtensionContext extensionContext, - ThrowableCollector throwableCollector) { + 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, registrar, throwableCollector); + extensionRegistryForOuterInstanceCreation, context); return instantiateTestClass(Optional.of(outerInstances), registry, extensionContext); } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java index b4f874966522..d9da3cb4da7a 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java @@ -101,19 +101,18 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte MethodExtensionContext extensionContext = new MethodExtensionContext(context.getExtensionContext(), context.getExecutionListener(), this, context.getConfiguration(), throwableCollector, it -> new DefaultExecutableInvoker(it, registry)); - throwableCollector.execute(() -> { - TestInstances testInstances = context.getTestInstancesProvider().getTestInstances(registry, - throwableCollector); - extensionContext.setTestInstances(testInstances); - }); - // @formatter:off - return context.extend() + JupiterEngineExecutionContext newContext = context.extend() .withExtensionRegistry(registry) .withExtensionContext(extensionContext) .withThrowableCollector(throwableCollector) .build(); // @formatter:on + throwableCollector.execute(() -> { + TestInstances testInstances = newContext.getTestInstancesProvider().getTestInstances(newContext); + extensionContext.setTestInstances(testInstances); + }); + return newContext; } protected MutableExtensionRegistry populateNewExtensionRegistry(JupiterEngineExecutionContext context) { diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ExtensionContextSupplier.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ExtensionContextSupplier.java new file mode 100644 index 000000000000..1a81c0df6536 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ExtensionContextSupplier.java @@ -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 + * #3445 (Introduction of Test-scoped ExtensionContext). + * + * @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; + } + } +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/InterceptingExecutableInvoker.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/InterceptingExecutableInvoker.java index e81c46c0715b..a070f2808c05 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/InterceptingExecutableInvoker.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/InterceptingExecutableInvoker.java @@ -53,8 +53,9 @@ public class InterceptingExecutableInvoker { * invocation via all registered {@linkplain InvocationInterceptor * interceptors} */ - public T invoke(Constructor constructor, Optional outerInstance, ExtensionContext extensionContext, - ExtensionRegistry extensionRegistry, ReflectiveInterceptorCall, T> interceptorCall) { + public T invoke(Constructor constructor, Optional outerInstance, + ExtensionContextSupplier extensionContext, ExtensionRegistry extensionRegistry, + ReflectiveInterceptorCall, T> interceptorCall) { Object[] arguments = resolveParameters(constructor, Optional.empty(), outerInstance, extensionContext, extensionRegistry); @@ -93,6 +94,14 @@ private T invoke(Invocation originalInvocation, wrappedInvocation) -> call.apply(interceptor, wrappedInvocation, invocationContext, extensionContext)); } + private T invoke(Invocation originalInvocation, + ReflectiveInvocationContext invocationContext, ExtensionContextSupplier extensionContext, + ExtensionRegistry extensionRegistry, ReflectiveInterceptorCall call) { + return interceptorChain.invoke(originalInvocation, extensionRegistry, + (interceptor, wrappedInvocation) -> call.apply(interceptor, wrappedInvocation, invocationContext, + extensionContext.get(interceptor))); + } + public interface ReflectiveInterceptorCall { T apply(InvocationInterceptor interceptor, Invocation invocation, diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ParameterResolutionUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ParameterResolutionUtils.java index 5d6e1b1802cc..93ee83d0e94b 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ParameterResolutionUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ParameterResolutionUtils.java @@ -83,6 +83,13 @@ public static Object[] resolveParameters(Method method, Optional target, */ public static Object[] resolveParameters(Executable executable, Optional target, Optional outerInstance, ExtensionContext extensionContext, ExtensionRegistry extensionRegistry) { + ExtensionContextSupplier context = new ExtensionContextSupplier(extensionContext, extensionContext); + return resolveParameters(executable, target, outerInstance, context, extensionRegistry); + } + + public static Object[] resolveParameters(Executable executable, Optional target, + Optional outerInstance, ExtensionContextSupplier extensionContext, + ExtensionRegistry extensionRegistry) { Preconditions.notNull(target, "target must not be null"); @@ -106,12 +113,12 @@ public static Object[] resolveParameters(Executable executable, Optional } private static Object resolveParameter(ParameterContext parameterContext, Executable executable, - ExtensionContext extensionContext, ExtensionRegistry extensionRegistry) { + ExtensionContextSupplier extensionContext, ExtensionRegistry extensionRegistry) { try { // @formatter:off List matchingResolvers = extensionRegistry.stream(ParameterResolver.class) - .filter(resolver -> resolver.supportsParameter(parameterContext, extensionContext)) + .filter(resolver -> resolver.supportsParameter(parameterContext, extensionContext.get(resolver))) .collect(toList()); // @formatter:on @@ -133,7 +140,7 @@ private static Object resolveParameter(ParameterContext parameterContext, Execut } ParameterResolver resolver = matchingResolvers.get(0); - Object value = resolver.resolveParameter(parameterContext, extensionContext); + Object value = resolver.resolveParameter(parameterContext, extensionContext.get(resolver)); validateResolvedType(parameterContext.getParameter(), value, executable, resolver); logger.trace(() -> String.format( diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/TestInstancesProvider.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/TestInstancesProvider.java index bceef6248a67..e8a87b3d88e2 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/TestInstancesProvider.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/TestInstancesProvider.java @@ -14,10 +14,7 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.extension.TestInstances; -import org.junit.jupiter.engine.extension.ExtensionRegistrar; import org.junit.jupiter.engine.extension.ExtensionRegistry; -import org.junit.jupiter.engine.extension.MutableExtensionRegistry; -import org.junit.platform.engine.support.hierarchical.ThrowableCollector; /** * @since 5.0 @@ -26,12 +23,10 @@ @API(status = INTERNAL, since = "5.0") public interface TestInstancesProvider { - default TestInstances getTestInstances(MutableExtensionRegistry extensionRegistry, - ThrowableCollector throwableCollector) { - return getTestInstances(extensionRegistry, extensionRegistry, throwableCollector); + default TestInstances getTestInstances(JupiterEngineExecutionContext context) { + return getTestInstances(context.getExtensionRegistry(), context); } - TestInstances getTestInstances(ExtensionRegistry extensionRegistry, ExtensionRegistrar extensionRegistrar, - ThrowableCollector throwableCollector); + TestInstances getTestInstances(ExtensionRegistry extensionRegistry, JupiterEngineExecutionContext executionContext); } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleTests.java index b03583f10fab..6358c0ad9ba3 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleTests.java @@ -992,8 +992,7 @@ public void postProcessTestInstance(Object testInstance, ExtensionContext contex trackLifecycle(context); assertThat(context.getTestInstance()).isNotPresent(); assertNotNull(testInstance); - instanceMap.put(postProcessTestInstanceKey(context.getRequiredTestClass()), - DefaultTestInstances.of(testInstance)); + instanceMap.put(postProcessTestInstanceKey(testInstance.getClass()), DefaultTestInstances.of(testInstance)); } @Override diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/InterceptingExecutableInvokerTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/InterceptingExecutableInvokerTests.java index 4ce912a74e73..c2dca032546a 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/InterceptingExecutableInvokerTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/InterceptingExecutableInvokerTests.java @@ -31,7 +31,8 @@ void invokeMethod() { @Override T invokeConstructor(Constructor constructor, Object outerInstance) { - return newInvoker().invoke(constructor, Optional.ofNullable(outerInstance), extensionContext, extensionRegistry, + ExtensionContextSupplier context = new ExtensionContextSupplier(extensionContext, extensionContext); + return newInvoker().invoke(constructor, Optional.ofNullable(outerInstance), context, extensionRegistry, passthroughInterceptor()); } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstanceFactoryTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstanceFactoryTests.java index 734f6e441664..48d4218faccd 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstanceFactoryTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstanceFactoryTests.java @@ -35,6 +35,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.EnableTestScopedConstructorContext; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtensionConfigurationException; import org.junit.jupiter.api.extension.ExtensionContext; @@ -276,7 +277,8 @@ void instanceFactoryOnTopLevelTestClass() { // @formatter:off assertThat(callSequence).containsExactly( "FooInstanceFactory instantiated: ParentTestCase", - "parentTest" + "parentTest", + "close ParentTestCase" ); // @formatter:on } @@ -305,8 +307,10 @@ void inheritedFactoryInTestClassHierarchy() { assertThat(callSequence).containsExactly( "FooInstanceFactory instantiated: InheritedFactoryTestCase", "parentTest", + "close InheritedFactoryTestCase", "FooInstanceFactory instantiated: InheritedFactoryTestCase", - "childTest" + "childTest", + "close InheritedFactoryTestCase" ); // @formatter:on } @@ -324,17 +328,23 @@ void instanceFactoriesInNestedClassStructureAreInherited() { // OuterTestCase "FooInstanceFactory instantiated: OuterTestCase", "outerTest", + "close OuterTestCase", // InnerTestCase "FooInstanceFactory instantiated: OuterTestCase", "FooInstanceFactory instantiated: InnerTestCase", "innerTest1", + "close InnerTestCase", + "close OuterTestCase", // InnerInnerTestCase "FooInstanceFactory instantiated: OuterTestCase", "FooInstanceFactory instantiated: InnerTestCase", "FooInstanceFactory instantiated: InnerInnerTestCase", - "innerTest2" + "innerTest2", + "close InnerInnerTestCase", + "close InnerTestCase", + "close OuterTestCase" ); // @formatter:on } @@ -349,7 +359,8 @@ void instanceFactoryRegisteredViaTestInterface() { // @formatter:off assertThat(callSequence).containsExactly( "FooInstanceFactory instantiated: FactoryFromInterfaceTestCase", - "test" + "test", + "close FactoryFromInterfaceTestCase" ); // @formatter:on } @@ -386,7 +397,34 @@ void instanceFactoryWithPerClassLifecycle() { "test1", "@BeforeEach", "test2", - "@AfterAll" + "@AfterAll", + "close PerClassLifecycleTestCase" + ); + // @formatter:on + } + + @Test + void instanceFactoryWithLegacyContext() { + EngineExecutionResults executionResults = executeTestsForClass(LegacyContextTestCase.class); + + assertEquals(3, executionResults.testEvents().started().count(), "# tests started"); + assertEquals(3, executionResults.testEvents().succeeded().count(), "# tests succeeded"); + + // @formatter:off + assertThat(callSequence).containsExactly( + "LegacyInstanceFactory instantiated: LegacyContextTestCase", + "outerTest", + "LegacyInstanceFactory instantiated: LegacyContextTestCase", + "LegacyInstanceFactory instantiated: InnerTestCase", + "innerTest1", + "LegacyInstanceFactory instantiated: LegacyContextTestCase", + "LegacyInstanceFactory instantiated: InnerTestCase", + "innerTest2", + "close InnerTestCase", + "close InnerTestCase", + "close LegacyContextTestCase", + "close LegacyContextTestCase", + "close LegacyContextTestCase" ); // @formatter:on } @@ -609,6 +647,29 @@ void afterAll() { } } + @ExtendWith(LegacyInstanceFactory.class) + static class LegacyContextTestCase { + + @Test + void outerTest() { + callSequence.add("outerTest"); + } + + @Nested + class InnerTestCase { + + @Test + void innerTest1() { + callSequence.add("innerTest1"); + } + + @Test + void innerTest2() { + callSequence.add("innerTest2"); + } + } + } + @ExtendWith(ProxyTestInstanceFactory.class) @TestInstance(PER_CLASS) static class ProxiedTestCase { @@ -633,6 +694,10 @@ public Object createTestInstance(TestInstanceFactoryContext factoryContext, Exte Class testClass = factoryContext.getTestClass(); instantiated(getClass(), testClass); + extensionContext.getStore(ExtensionContext.Namespace.create(this)).put(new Object(), + (ExtensionContext.Store.CloseableResource) () -> callSequence.add( + "close " + testClass.getSimpleName())); + if (factoryContext.getOuterInstance().isPresent()) { return ReflectionSupport.newInstance(testClass, factoryContext.getOuterInstance().get()); } @@ -641,12 +706,17 @@ public Object createTestInstance(TestInstanceFactoryContext factoryContext, Exte } } + @EnableTestScopedConstructorContext private static class FooInstanceFactory extends AbstractTestInstanceFactory { } + @EnableTestScopedConstructorContext private static class BarInstanceFactory extends AbstractTestInstanceFactory { } + private static class LegacyInstanceFactory extends AbstractTestInstanceFactory { + } + /** * {@link TestInstanceFactory} that returns null. */ diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePostProcessorTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePostProcessorTests.java index a9b4fe75cf03..416e90e8e532 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePostProcessorTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePostProcessorTests.java @@ -14,11 +14,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.EnableTestScopedConstructorContext; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestInstancePostProcessor; @@ -46,18 +49,28 @@ void instancePostProcessorsInNestedClasses() { assertThat(callSequence).containsExactly( // OuterTestCase - "fooPostProcessTestInstance:OuterTestCase", + "foo:OuterTestCase", + "legacy:OuterTestCase", "beforeOuterMethod", "testOuter", + "close:foo:OuterTestCase", // InnerTestCase - "fooPostProcessTestInstance:OuterTestCase", - "fooPostProcessTestInstance:InnerTestCase", - "barPostProcessTestInstance:InnerTestCase", + "foo:OuterTestCase", + "legacy:OuterTestCase", + "foo:InnerTestCase", + "legacy:InnerTestCase", + "bar:InnerTestCase", "beforeOuterMethod", "beforeInnerMethod", - "testInner" + "testInner", + "close:bar:InnerTestCase", + "close:foo:InnerTestCase", + "close:foo:OuterTestCase", + "close:legacy:InnerTestCase", + "close:legacy:OuterTestCase", + "close:legacy:OuterTestCase" ); // @formatter:on } @@ -65,22 +78,33 @@ void instancePostProcessorsInNestedClasses() { @Test void testSpecificTestInstancePostProcessorIsCalled() { executeTestsForClass(TestCaseWithTestSpecificTestInstancePostProcessor.class).testEvents()// - .assertStatistics(stats -> stats.started(1).succeeded(1)); + .assertStatistics(stats -> stats.started(2).succeeded(2)); + // @formatter:off assertThat(callSequence).containsExactly( - "fooPostProcessTestInstance:TestCaseWithTestSpecificTestInstancePostProcessor", "beforeEachMethod", "test"); + "foo:TestCaseWithTestSpecificTestInstancePostProcessor", + "legacy:TestCaseWithTestSpecificTestInstancePostProcessor", + "beforeEachMethod", + "test1", + "close:foo:TestCaseWithTestSpecificTestInstancePostProcessor", + "beforeEachMethod", + "test2", + "close:legacy:TestCaseWithTestSpecificTestInstancePostProcessor" + ); + // @formatter:on } // ------------------------------------------------------------------- @ExtendWith(FooInstancePostProcessor.class) + @ExtendWith(LegacyInstancePostProcessor.class) static class OuterTestCase implements Named { - private String outerName; + private final Map outerNames = new HashMap<>(); @Override - public void setName(String name) { - this.outerName = name; + public void setName(String source, String name) { + outerNames.put(source, name); } @BeforeEach @@ -90,7 +114,9 @@ void beforeOuterMethod() { @Test void testOuter() { - assertEquals("foo:" + OuterTestCase.class.getSimpleName(), outerName); + assertEquals( + Map.of("foo", OuterTestCase.class.getSimpleName(), "legacy", OuterTestCase.class.getSimpleName()), + outerNames); callSequence.add("testOuter"); } @@ -98,11 +124,11 @@ void testOuter() { @ExtendWith(BarInstancePostProcessor.class) class InnerTestCase implements Named { - private String innerName; + private final Map innerNames = new HashMap<>(); @Override - public void setName(String name) { - this.innerName = name; + public void setName(String source, String name) { + innerNames.put(source, name); } @BeforeEach @@ -112,8 +138,11 @@ void beforeInnerMethod() { @Test void testInner() { - assertEquals("foo:" + OuterTestCase.class.getSimpleName(), outerName); - assertEquals("bar:" + InnerTestCase.class.getSimpleName(), innerName); + assertEquals( + Map.of("foo", InnerTestCase.class.getSimpleName(), "legacy", OuterTestCase.class.getSimpleName()), + outerNames); + assertEquals(Map.of("foo", InnerTestCase.class.getSimpleName(), "bar", + InnerTestCase.class.getSimpleName(), "legacy", InnerTestCase.class.getSimpleName()), innerNames); callSequence.add("testInner"); } } @@ -122,11 +151,11 @@ void testInner() { static class TestCaseWithTestSpecificTestInstancePostProcessor implements Named { - private String name; + private final Map names = new HashMap<>(); @Override - public void setName(String name) { - this.name = name; + public void setName(String source, String name) { + names.put(source, name); } @BeforeEach @@ -135,38 +164,63 @@ void beforeEachMethod() { } @ExtendWith(FooInstancePostProcessor.class) + @ExtendWith(LegacyInstancePostProcessor.class) + @Test + void test1() { + callSequence.add("test1"); + assertEquals(Map.of("foo", getClass().getSimpleName(), "legacy", getClass().getSimpleName()), names); + } + @Test - void test() { - callSequence.add("test"); - assertEquals("foo:" + getClass().getSimpleName(), name); + void test2() { + callSequence.add("test2"); + assertEquals(Map.of(), names); } } - static class FooInstancePostProcessor implements TestInstancePostProcessor { + static abstract class AbstractInstancePostProcessor implements TestInstancePostProcessor { + private final String name; + + AbstractInstancePostProcessor(String name) { + this.name = name; + } @Override public void postProcessTestInstance(Object testInstance, ExtensionContext context) { if (testInstance instanceof Named) { - ((Named) testInstance).setName("foo:" + context.getRequiredTestClass().getSimpleName()); + ((Named) testInstance).setName(name, context.getRequiredTestClass().getSimpleName()); } - callSequence.add("fooPostProcessTestInstance:" + testInstance.getClass().getSimpleName()); + String instanceType = testInstance.getClass().getSimpleName(); + callSequence.add(name + ":" + instanceType); + context.getStore(ExtensionContext.Namespace.create(this)).put(new Object(), + (ExtensionContext.Store.CloseableResource) () -> callSequence.add( + "close:" + name + ":" + instanceType)); } } - static class BarInstancePostProcessor implements TestInstancePostProcessor { + @EnableTestScopedConstructorContext + static class FooInstancePostProcessor extends AbstractInstancePostProcessor { + FooInstancePostProcessor() { + super("foo"); + } + } - @Override - public void postProcessTestInstance(Object testInstance, ExtensionContext context) { - if (testInstance instanceof Named) { - ((Named) testInstance).setName("bar:" + context.getRequiredTestClass().getSimpleName()); - } - callSequence.add("barPostProcessTestInstance:" + testInstance.getClass().getSimpleName()); + @EnableTestScopedConstructorContext + static class BarInstancePostProcessor extends AbstractInstancePostProcessor { + BarInstancePostProcessor() { + super("bar"); + } + } + + static class LegacyInstancePostProcessor extends AbstractInstancePostProcessor { + LegacyInstancePostProcessor() { + super("legacy"); } } private interface Named { - void setName(String name); + void setName(String source, String name); } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePreConstructCallbackTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePreConstructCallbackTests.java index fdf323d21291..4da0833bd0d7 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePreConstructCallbackTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePreConstructCallbackTests.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.EnableTestScopedConstructorContext; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.RegisterExtension; @@ -59,12 +60,14 @@ void instancePreConstruct() { "beforeEach", "test1", "afterEach", + "close: name=foo, testClass=InstancePreConstructTestCase", "PreConstructCallback: name=foo, testClass=InstancePreConstructTestCase, outerInstance: null", "constructor", "beforeEach", "test2", "afterEach", + "close: name=foo, testClass=InstancePreConstructTestCase", "afterAll" ); @@ -86,6 +89,7 @@ void factoryPreConstruct() { "beforeEach", "test1", "afterEach", + "close: name=foo, testClass=FactoryPreConstructTestCase", "PreConstructCallback: name=foo, testClass=FactoryPreConstructTestCase, outerInstance: null", "testInstanceFactory", @@ -93,6 +97,7 @@ void factoryPreConstruct() { "beforeEach", "test2", "afterEach", + "close: name=foo, testClass=FactoryPreConstructTestCase", "afterAll" ); @@ -113,12 +118,14 @@ void preConstructInNested() { "beforeEach", "outerTest1", "afterEach", + "close: name=foo, testClass=PreConstructInNestedTestCase", "PreConstructCallback: name=foo, testClass=PreConstructInNestedTestCase, outerInstance: null", "constructor", "beforeEach", "outerTest2", "afterEach", + "close: name=foo, testClass=PreConstructInNestedTestCase", "PreConstructCallback: name=foo, testClass=PreConstructInNestedTestCase, outerInstance: null", "constructor", @@ -133,6 +140,11 @@ void preConstructInNested() { "afterEachInner", "afterEach", + "close: name=baz, testClass=InnerTestCase", + "close: name=bar, testClass=InnerTestCase", + "close: name=foo, testClass=InnerTestCase", + "close: name=foo, testClass=PreConstructInNestedTestCase", + "afterAll" ); // @formatter:on @@ -150,6 +162,7 @@ void preConstructOnMethod() { "beforeEach", "test1", "afterEach", + "close: name=foo, testClass=PreConstructOnMethod", "constructor", "beforeEach", @@ -172,7 +185,57 @@ void preConstructWithClassLifecycle() { "beforeEach", "test1", "beforeEach", - "test2" + "test2", + "close: name=bar, testClass=PreConstructWithClassLifecycle", + "close: name=foo, testClass=PreConstructWithClassLifecycle" + ); + // @formatter:on + } + + @Test + void legacyPreConstruct() { + executeTestsForClass(LegacyPreConstructTestCase.class).testEvents()// + .assertStatistics(stats -> stats.started(3).succeeded(3)); + + // @formatter:off + assertThat(callSequence).containsExactly( + "beforeAll", + + "PreConstructCallback: name=foo, testClass=LegacyPreConstructTestCase, outerInstance: null", + "PreConstructCallback: name=legacy, testClass=LegacyPreConstructTestCase, outerInstance: null", + "constructor", + "beforeEach", + "outerTest1", + "afterEach", + "close: name=foo, testClass=LegacyPreConstructTestCase", + + "PreConstructCallback: name=foo, testClass=LegacyPreConstructTestCase, outerInstance: null", + "PreConstructCallback: name=legacy, testClass=LegacyPreConstructTestCase, outerInstance: null", + "constructor", + "beforeEach", + "outerTest2", + "afterEach", + "close: name=foo, testClass=LegacyPreConstructTestCase", + + "PreConstructCallback: name=foo, testClass=LegacyPreConstructTestCase, outerInstance: null", + "PreConstructCallback: name=legacy, testClass=LegacyPreConstructTestCase, outerInstance: null", + "constructor", + "PreConstructCallback: name=foo, testClass=InnerTestCase, outerInstance: LegacyPreConstructTestCase", + "PreConstructCallback: name=legacy, testClass=InnerTestCase, outerInstance: LegacyPreConstructTestCase", + "constructorInner", + "beforeEach", + "beforeEachInner", + "innerTest1", + "afterEachInner", + "afterEach", + "close: name=foo, testClass=InnerTestCase", + "close: name=foo, testClass=LegacyPreConstructTestCase", + + "close: name=legacy, testClass=InnerTestCase", + "afterAll", + "close: name=legacy, testClass=LegacyPreConstructTestCase", + "close: name=legacy, testClass=LegacyPreConstructTestCase", + "close: name=legacy, testClass=LegacyPreConstructTestCase" ); // @formatter:on } @@ -393,6 +456,73 @@ void test2() { } } + @ExtendWith(InstancePreConstructCallbackRecordingFoo.class) + @ExtendWith(InstancePreConstructCallbackRecordingLegacy.class) + static class LegacyPreConstructTestCase extends CallSequenceRecordingTestCase { + + LegacyPreConstructTestCase() { + record("constructor"); + } + + @BeforeAll + static void beforeAll() { + record("beforeAll"); + } + + @BeforeEach + void beforeEach() { + record("beforeEach"); + } + + @Test + void outerTest1() { + record("outerTest1"); + } + + @Test + void outerTest2() { + record("outerTest2"); + } + + @AfterEach + void afterEach() { + record("afterEach"); + } + + @AfterAll + static void afterAll() { + record("afterAll"); + } + + @Override + public String toString() { + return "LegacyPreConstructTestCase"; + } + + @Nested + class InnerTestCase extends CallSequenceRecordingTestCase { + + InnerTestCase() { + record("constructorInner"); + } + + @BeforeEach + void beforeEachInner() { + record("beforeEachInner"); + } + + @Test + void innerTest1() { + record("innerTest1"); + } + + @AfterEach + void afterEachInner() { + record("afterEachInner"); + } + } + } + static abstract class AbstractTestInstancePreConstructCallback implements TestInstancePreConstructCallback { private final String name; @@ -404,29 +534,49 @@ static abstract class AbstractTestInstancePreConstructCallback implements TestIn public void preConstructTestInstance(TestInstanceFactoryContext factoryContext, ExtensionContext context) { assertThat(context.getTestInstance()).isNotPresent(); assertThat(context.getTestClass()).isPresent(); - assertThat(factoryContext.getTestClass()).isSameAs(context.getTestClass().get()); - callSequence.add( - "PreConstructCallback: name=" + name + ", testClass=" + factoryContext.getTestClass().getSimpleName() - + ", outerInstance: " + factoryContext.getOuterInstance().orElse(null)); + if (name.equals("legacy")) { + assertThat(factoryContext.getTestClass()).isSameAs(context.getTestClass().get()); + } + else if (context.getTestInstanceLifecycle().orElse(null) != TestInstance.Lifecycle.PER_CLASS) { + assertThat(context.getTestMethod()).isPresent(); + } + else { + assertThat(context.getTestMethod()).isEmpty(); + } + String testClass = factoryContext.getTestClass().getSimpleName(); + callSequence.add("PreConstructCallback: name=" + name + ", testClass=" + testClass + ", outerInstance: " + + factoryContext.getOuterInstance().orElse(null)); + context.getStore(ExtensionContext.Namespace.create(this)).put(new Object(), + (ExtensionContext.Store.CloseableResource) () -> callSequence.add( + "close: name=" + name + ", testClass=" + testClass)); } } + @EnableTestScopedConstructorContext static class InstancePreConstructCallbackRecordingFoo extends AbstractTestInstancePreConstructCallback { InstancePreConstructCallbackRecordingFoo() { super("foo"); } } + @EnableTestScopedConstructorContext static class InstancePreConstructCallbackRecordingBar extends AbstractTestInstancePreConstructCallback { InstancePreConstructCallbackRecordingBar() { super("bar"); } } + @EnableTestScopedConstructorContext static class InstancePreConstructCallbackRecordingBaz extends AbstractTestInstancePreConstructCallback { InstancePreConstructCallbackRecordingBaz() { super("baz"); } } + static class InstancePreConstructCallbackRecordingLegacy extends AbstractTestInstancePreConstructCallback { + InstancePreConstructCallbackRecordingLegacy() { + super("legacy"); + } + } + }