From 2744efba10d29552029df29be3481874619500c2 Mon Sep 17 00:00:00 2001 From: Johannes Spangenberg Date: Sat, 5 Oct 2024 03:09:59 +0200 Subject: [PATCH] Add annotation for legacy and documentation Issue: #3445 --- .../src/docs/asciidoc/link-attributes.adoc | 1 + .../release-notes-5.12.0-M1.adoc | 11 +- .../docs/asciidoc/user-guide/extensions.adoc | 18 +++ .../EnableTestScopedConstructorContext.java | 76 ++++++++++ .../api/extension/TestInstanceFactory.java | 6 + .../extension/TestInstancePostProcessor.java | 6 + .../TestInstancePreConstructCallback.java | 6 + .../descriptor/ClassBasedTestDescriptor.java | 65 ++++++--- .../descriptor/ClassTestDescriptor.java | 5 +- .../descriptor/NestedClassTestDescriptor.java | 6 +- .../extension/TestInstanceFactoryTests.java | 55 ++++++++ .../TestInstancePostProcessorTests.java | 97 ++++++++----- ...TestInstancePreConstructCallbackTests.java | 130 +++++++++++++++++- 13 files changed, 425 insertions(+), 57 deletions(-) create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/EnableTestScopedConstructorContext.java diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc index dfbe7932d479..a9d0addbcbba 100644 --- a/documentation/src/docs/asciidoc/link-attributes.adoc +++ b/documentation/src/docs/asciidoc/link-attributes.adoc @@ -138,6 +138,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 e23dcb707c34..efb31ed237e8 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 @@ -42,7 +42,10 @@ 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 @@ -54,6 +57,12 @@ 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. [[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..cac594400001 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 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..5688d35fba54 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/EnableTestScopedConstructorContext.java @@ -0,0 +1,76 @@ +/* + * 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)}. + * + *

+ * + *

Implementations of these extension callbacks can observe the following + * differences if they are using {@code @EnableTestScopedConstructorContext}. + * + *

+ * + *

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 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/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 fe944c10739e..2a89cd7e3a7b 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 @@ -39,6 +39,7 @@ 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; @@ -66,6 +67,7 @@ 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; @@ -277,15 +279,18 @@ private TestInstancesProvider testInstancesProvider(JupiterEngineExecutionContex // 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, registry, context)); + () -> instantiateAndPostProcessTestInstance(parentExecutionContext, ourExtensionContext, registry, + context)); } private TestInstances instantiateAndPostProcessTestInstance(JupiterEngineExecutionContext parentExecutionContext, - ExtensionRegistry registry, JupiterEngineExecutionContext context) { + ClassExtensionContext ourExtensionContext, ExtensionRegistry registry, + JupiterEngineExecutionContext context) { - TestInstances instances = instantiateTestClass(parentExecutionContext, registry, context); + TestInstances instances = instantiateTestClass(parentExecutionContext, ourExtensionContext, registry, context); context.getThrowableCollector().execute(() -> { - invokeTestInstancePostProcessors(instances.getInnermostInstance(), registry, context.getExtensionContext()); + invokeTestInstancePostProcessors(instances.getInnermostInstance(), registry, context.getExtensionContext(), + ourExtensionContext); // 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. @@ -295,27 +300,35 @@ private TestInstances instantiateAndPostProcessTestInstance(JupiterEngineExecuti } protected abstract TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext, - ExtensionRegistry registry, JupiterEngineExecutionContext context); + ExtensionContext ourExtensionContext, ExtensionRegistry registry, JupiterEngineExecutionContext context); protected TestInstances instantiateTestClass(Optional outerInstances, ExtensionRegistry registry, - ExtensionContext extensionContext) { + ExtensionContext extensionContext, ExtensionContext ourExtensionContext) { Optional outerInstance = outerInstances.map(TestInstances::getInnermostInstance); invokeTestInstancePreConstructCallbacks(new DefaultTestInstanceFactoryContext(this.testClass, outerInstance), - registry, extensionContext); + registry, extensionContext, ourExtensionContext); Object instance = this.testInstanceFactory != null // - ? invokeTestInstanceFactory(outerInstance, extensionContext) // - : invokeTestClassConstructor(outerInstance, registry, extensionContext); + ? invokeTestInstanceFactory(outerInstance, extensionContext, ourExtensionContext) // + : invokeTestClassConstructor(outerInstance, registry, extensionContext, ourExtensionContext); return outerInstances.map(instances -> DefaultTestInstances.of(instances, instance)).orElse( DefaultTestInstances.of(instance)); } - private Object invokeTestInstanceFactory(Optional outerInstance, ExtensionContext extensionContext) { + private Object invokeTestInstanceFactory(Optional outerInstance, ExtensionContext extensionContext, + ExtensionContext ourExtensionContext) { Object instance; try { - instance = this.testInstanceFactory.createTestInstance( - new DefaultTestInstanceFactoryContext(this.testClass, outerInstance), extensionContext); + 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); + } } catch (Throwable throwable) { UnrecoverableExceptions.rethrowIfUnrecoverable(throwable); @@ -355,7 +368,7 @@ private Object invokeTestInstanceFactory(Optional outerInstance, Extensi } private Object invokeTestClassConstructor(Optional outerInstance, ExtensionRegistry registry, - ExtensionContext extensionContext) { + ExtensionContext extensionContext, ExtensionContext ourExtensionContext) { Constructor constructor = ReflectionUtils.getDeclaredConstructor(this.testClass); return executableInvoker.invoke(constructor, outerInstance, extensionContext, registry, @@ -363,16 +376,28 @@ 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, 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)); + } + }); } - private void invokeTestInstancePostProcessors(Object instance, ExtensionRegistry registry, - ExtensionContext context) { + private void invokeTestInstancePostProcessors(Object instance, ExtensionRegistry registry, ExtensionContext context, + ClassExtensionContext ourContext) { - registry.stream(TestInstancePostProcessor.class).forEach( - extension -> executeAndMaskThrowable(() -> extension.postProcessTestInstance(instance, 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)); + } + }); } 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 97fbf790120e..9b1afeb2de95 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,6 +20,7 @@ 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.JupiterEngineExecutionContext; @@ -71,8 +72,8 @@ public ExecutionMode getExecutionMode() { @Override protected TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext, - ExtensionRegistry registry, JupiterEngineExecutionContext context) { - return instantiateTestClass(Optional.empty(), registry, context.getExtensionContext()); + ExtensionContext ourExtensionContext, ExtensionRegistry registry, JupiterEngineExecutionContext context) { + return instantiateTestClass(Optional.empty(), registry, context.getExtensionContext(), ourExtensionContext); } } 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 8532fa2d75a6..aa86ab3a11e1 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,6 +21,7 @@ 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.JupiterEngineExecutionContext; @@ -74,13 +75,14 @@ public List> getEnclosingTestClasses() { @Override protected TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext, - ExtensionRegistry registry, JupiterEngineExecutionContext context) { + ExtensionContext ourExtensionContext, 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()); + return instantiateTestClass(Optional.of(outerInstances), registry, context.getExtensionContext(), + ourExtensionContext); } } 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 75c6fc5ed9d1..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; @@ -402,6 +403,32 @@ void instanceFactoryWithPerClassLifecycle() { // @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 + } + // ------------------------------------------------------------------------- @ExtendWith({ FooInstanceFactory.class, BarInstanceFactory.class }) @@ -620,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 { @@ -656,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 ab0f534c1ff1..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,22 +49,28 @@ void instancePostProcessorsInNestedClasses() { assertThat(callSequence).containsExactly( // OuterTestCase - "fooPostProcessTestInstance:OuterTestCase", + "foo:OuterTestCase", + "legacy:OuterTestCase", "beforeOuterMethod", "testOuter", - "close:OuterTestCase", + "close:foo:OuterTestCase", // InnerTestCase - "fooPostProcessTestInstance:OuterTestCase", - "fooPostProcessTestInstance:InnerTestCase", - "barPostProcessTestInstance:InnerTestCase", + "foo:OuterTestCase", + "legacy:OuterTestCase", + "foo:InnerTestCase", + "legacy:InnerTestCase", + "bar:InnerTestCase", "beforeOuterMethod", "beforeInnerMethod", "testInner", - "close:InnerTestCase", - "close:InnerTestCase", - "close:OuterTestCase" + "close:bar:InnerTestCase", + "close:foo:InnerTestCase", + "close:foo:OuterTestCase", + "close:legacy:InnerTestCase", + "close:legacy:OuterTestCase", + "close:legacy:OuterTestCase" ); // @formatter:on } @@ -69,14 +78,18 @@ 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", + "foo:TestCaseWithTestSpecificTestInstancePostProcessor", + "legacy:TestCaseWithTestSpecificTestInstancePostProcessor", "beforeEachMethod", - "test", - "close:TestCaseWithTestSpecificTestInstancePostProcessor" + "test1", + "close:foo:TestCaseWithTestSpecificTestInstancePostProcessor", + "beforeEachMethod", + "test2", + "close:legacy:TestCaseWithTestSpecificTestInstancePostProcessor" ); // @formatter:on } @@ -84,13 +97,14 @@ void testSpecificTestInstancePostProcessorIsCalled() { // ------------------------------------------------------------------- @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 @@ -100,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"); } @@ -108,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 @@ -122,8 +138,11 @@ void beforeInnerMethod() { @Test void testInner() { - assertEquals("foo:" + InnerTestCase.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"); } } @@ -132,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 @@ -145,10 +164,17 @@ 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); } } @@ -162,30 +188,39 @@ static abstract class AbstractInstancePostProcessor implements TestInstancePostP @Override public void postProcessTestInstance(Object testInstance, ExtensionContext context) { if (testInstance instanceof Named) { - ((Named) testInstance).setName(name + ":" + context.getRequiredTestClass().getSimpleName()); + ((Named) testInstance).setName(name, context.getRequiredTestClass().getSimpleName()); } String instanceType = testInstance.getClass().getSimpleName(); - callSequence.add(name + "PostProcessTestInstance:" + instanceType); + callSequence.add(name + ":" + instanceType); context.getStore(ExtensionContext.Namespace.create(this)).put(new Object(), - (ExtensionContext.Store.CloseableResource) () -> callSequence.add("close:" + instanceType)); + (ExtensionContext.Store.CloseableResource) () -> callSequence.add( + "close:" + name + ":" + instanceType)); } } + @EnableTestScopedConstructorContext static class FooInstancePostProcessor extends AbstractInstancePostProcessor { FooInstancePostProcessor() { super("foo"); } } + @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 2f69660b705f..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; @@ -191,6 +192,54 @@ void 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 + } + private abstract static class CallSequenceRecordingTestCase { protected static void record(String event) { callSequence.add(event); @@ -407,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; @@ -418,7 +534,10 @@ static abstract class AbstractTestInstancePreConstructCallback implements TestIn public void preConstructTestInstance(TestInstanceFactoryContext factoryContext, ExtensionContext context) { assertThat(context.getTestInstance()).isNotPresent(); assertThat(context.getTestClass()).isPresent(); - if (context.getTestInstanceLifecycle().orElse(null) != TestInstance.Lifecycle.PER_CLASS) { + 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 { @@ -433,22 +552,31 @@ public void preConstructTestInstance(TestInstanceFactoryContext factoryContext, } } + @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"); + } + } + }