diff --git a/framework-docs/modules/ROOT/pages/appendix.adoc b/framework-docs/modules/ROOT/pages/appendix.adoc index dcfaacb27b3..b65a9ab4c45 100644 --- a/framework-docs/modules/ROOT/pages/appendix.adoc +++ b/framework-docs/modules/ROOT/pages/appendix.adoc @@ -138,6 +138,11 @@ xref:testing/testcontext-framework/ctx-management/context-pausing.adoc[Context P in the _Spring TestContext Framework_. See xref:testing/testcontext-framework/ctx-management/failure-threshold.adoc[Context Failure Threshold]. +| `spring.test.extension.context.scope` +| The default _extension context scope_ used by the `SpringExtension` in `@Nested` test +class hierarchies. See +xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-springextensionconfig[`@SpringExtensionConfig`]. + | `spring.test.enclosing.configuration` | The default _enclosing configuration inheritance mode_ to use if `@NestedTestConfiguration` is not present on a test class. See diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc index 03ea705eaab..1ae4d6c4e1b 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc @@ -3,7 +3,7 @@ The following annotations are supported when used in conjunction with the xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`] -and JUnit Jupiter (that is, the programming model in JUnit): +and the JUnit Jupiter testing framework: * xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-springextensionconfig[`@SpringExtensionConfig`] * xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-junit-jupiter-springjunitconfig[`@SpringJUnitConfig`] @@ -30,30 +30,40 @@ developer wishes to switch to test-class scoped semantics — the `SpringExtensi configured to use a test-class scoped `ExtensionContext` by annotating a top-level test class with `@SpringExtensionConfig(useTestClassScopedExtensionContext = true)`. +Alternatively, you can change the global default by setting the +`spring.test.extension.context.scope` property to `test_class`. The property is resolved +first via the +{spring-framework-api}/org/springframework/core/SpringProperties.html[`SpringProperties`] +mechanism (in a `spring.properties` file on the classpath or via JVM system properties, +for example `-Dspring.test.extension.context.scope=test_class`). If the Spring property +has not been set, the `SpringExtension` will attempt to resolve the property as a +https://docs.junit.org/current/running-tests/configuration-parameters.html[JUnit Platform configuration parameter] +as a fallback mechanism. If the property has not been set via either of those mechanisms, +the `SpringExtension` will use a test-method scoped extension context by default. Note, +however, that a `@SpringExtensionConfig` declaration always takes precedence over this +property. + [TIP] ==== -If your top-level test class is configured to use JUnit Jupiter's -`@TestInstance(Lifecycle.PER_CLASS)` semantics, the `SpringExtension` will always use a -test-class scoped `ExtensionContext`, and there is no need to declare -`@SpringExtensionConfig(useTestClassScopedExtensionContext = true)`. +If a test class uses JUnit Jupiter's `@TestInstance(Lifecycle.PER_CLASS)` semantics, the +`SpringExtension` will always use a test-class scoped `ExtensionContext`, and +configuration via `@SpringExtensionConfig(useTestClassScopedExtensionContext = true)` or +the `spring.test.extension.context.scope` property will have no effect for that test +class. ==== [NOTE] ==== This annotation is currently only applicable to `@Nested` test class hierarchies and should be applied to the top-level enclosing class of a `@Nested` test class hierarchy. - Consequently, there is no need to declare this annotation on a test class that does not contain `@Nested` test classes. -==== -[NOTE] -==== +In addition, xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[`@NestedTestConfiguration`] -does not apply to this annotation. - -`@SpringExtensionConfig` will always be detected within a `@Nested` test class hierarchy, -effectively disregarding any `@NestedTestConfiguration(OVERRIDE)` declarations. +does not apply to this annotation. `@SpringExtensionConfig` will always be detected +within a `@Nested` test class hierarchy, effectively disregarding any +`@NestedTestConfiguration(OVERRIDE)` declarations. ==== [[integration-testing-annotations-junit-jupiter-springjunitconfig]] diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc index 54b9cd7b5cf..5f77d9e08b4 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc @@ -396,11 +396,10 @@ recursively. ==== As of Spring Framework 7.0, the `SpringExtension` uses a test-method scoped `ExtensionContext` within `@Nested` test class hierarchies by default. However, the -`SpringExtension` can be configured to use a test-class scoped `ExtensionContext`. - -See the documentation for -xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-springextensionconfig[`@SpringExtensionConfig`] -for details. +`SpringExtension` can be configured to use a test-class scoped `ExtensionContext` — for +example via `@SpringExtensionConfig` or the `spring.test.extension.context.scope` Spring +property (see +xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-springextensionconfig[`@SpringExtensionConfig`]). ==== [TIP] diff --git a/spring-test/src/main/java/org/springframework/test/context/TestConstructor.java b/spring-test/src/main/java/org/springframework/test/context/TestConstructor.java index 1e96aeb6e3d..9e667d4d24c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestConstructor.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestConstructor.java @@ -87,8 +87,7 @@ public @interface TestConstructor { * semantics by default. *

May alternatively be configured via the * {@link org.springframework.core.SpringProperties SpringProperties} - * mechanism. - *

This property may also be configured as a + * mechanism or as a * JUnit * Platform configuration parameter. * @see #autowireMode diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java index 84a68a76ab1..8a9f35d410d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java @@ -23,6 +23,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.Arrays; import java.util.List; +import java.util.Locale; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterAll; @@ -42,12 +43,14 @@ import org.junit.jupiter.api.extension.ExtensionContext.Store; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.api.extension.TestInstancePostProcessor; +import org.junit.jupiter.api.extension.TestInstantiationAwareExtension; import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.platform.commons.annotation.Testable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.ParameterResolutionDelegate; import org.springframework.context.ApplicationContext; +import org.springframework.core.SpringProperties; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; @@ -61,9 +64,9 @@ import org.springframework.test.context.support.PropertyProvider; import org.springframework.test.context.support.TestConstructorUtils; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.ConcurrentLruCache; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.MethodFilter; +import org.springframework.util.StringUtils; /** * {@code SpringExtension} integrates the Spring TestContext Framework @@ -85,15 +88,15 @@ import org.springframework.util.ReflectionUtils.MethodFilter; * TestExecutionListener} is not compatible with the semantics associated with * a test-method scoped extension context — or if a developer wishes to * switch to test-class scoped semantics — the {@code SpringExtension} can - * be configured to use a test-class scoped extension context by annotating a - * top-level test class with + * be configured by annotating a top-level test class with * {@link SpringExtensionConfig#useTestClassScopedExtensionContext() - * @SpringExtensionConfig(useTestClassScopedExtensionContext = true)}. Note, - * however, that the {@code SpringExtension} will always use a test-class scoped - * {@code ExtensionContext} if your top-level test class is configured to use JUnit - * Jupiter’s {@code @TestInstance(Lifecycle.PER_CLASS)} semantics, in which case - * there is no need to declare - * {@code @SpringExtensionConfig(useTestClassScopedExtensionContext = true)}. + * @SpringExtensionConfig(useTestClassScopedExtensionContext = true)} or by + * setting the {@value #EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME} property to + * {@link ExtensionContextScope#TEST_CLASS test_class}. Note that an explicit + * {@code @SpringExtensionConfig} declaration overrides the globally configured + * property. Furthermore, the {@code SpringExtension} will always use a test-class + * scoped {@code ExtensionContext} for a test class that is configured to use JUnit + * Jupiter’s {@code @TestInstance(Lifecycle.PER_CLASS)} semantics. * *

NOTE: This class requires JUnit Jupiter 6.0 or higher. * @@ -112,12 +115,42 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes BeforeEachCallback, AfterEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver { + /** + * JVM system property used to configure the default {@link ExtensionContextScope} + * for the {@code SpringExtension}: {@value}. + *

Acceptable values include enum constants defined in {@code ExtensionContextScope}, + * ignoring case. For example, the default may be changed to + * {@link ExtensionContextScope#TEST_CLASS} by supplying the following JVM + * system property via the command line. + *

-Dspring.test.extension.context.scope=test_class
+ *

If the property is not set, {@link ExtensionContextScope#TEST_METHOD} + * semantics will apply. Note, however, that {@code @SpringExtensionConfig} + * takes precedence over this property. + *

May alternatively be configured via the + * {@link org.springframework.core.SpringProperties SpringProperties} + * mechanism or as a + * JUnit + * Platform configuration parameter. + * @since 7.0.7 + * @see ExtensionContextScope + * @see SpringExtensionConfig + */ + public static final String EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME = "spring.test.extension.context.scope"; + /** * {@link Namespace} in which {@code TestContextManagers} are stored, keyed * by test class. */ private static final Namespace TEST_CONTEXT_MANAGER_NAMESPACE = Namespace.create(SpringExtension.class); + /** + * {@link Namespace} in which the resolved default {@link ExtensionContextScope} + * is stored. + * @since 7.0.7 + */ + private static final Namespace DEFAULT_EXTENSION_CONTEXT_SCOPE_NAMESPACE = + Namespace.create(SpringExtension.class.getName() + "#default.extension.context.scope"); + /** * {@link Namespace} in which {@code @Autowired} validation error messages * are stored, keyed by test class. @@ -139,14 +172,6 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes private static final Namespace RECORD_APPLICATION_EVENTS_VALIDATION_NAMESPACE = Namespace.create(SpringExtension.class.getName() + "#recordApplicationEvents.validation"); - /** - * LRU cache for {@link SpringExtensionConfig#useTestClassScopedExtensionContext()} - * mappings, keyed by test class. - * @since 7.0 - */ - private static final ConcurrentLruCache, Boolean> useTestClassScopedExtensionContextCache = - new ConcurrentLruCache<>(32, SpringExtension::useTestClassScopedExtensionContext); - // Note that @Test, @TestFactory, @TestTemplate, @RepeatedTest, and @ParameterizedTest // are all meta-annotated with @Testable. private static final List> JUPITER_ANNOTATION_TYPES = @@ -157,16 +182,23 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes /** - * Returns {@link ExtensionContextScope#TEST_METHOD ExtensionContextScope.TEST_METHOD}. - *

This can be effectively overridden by annotating a test class with - * {@code @SpringExtensionConfig(useTestClassScopedExtensionContext = true)}. - * See the {@linkplain SpringExtension class-level Javadoc} for further details. + * Returns {@link TestInstantiationAwareExtension.ExtensionContextScope#TEST_METHOD + * ExtensionContextScope.TEST_METHOD}. + *

This can be overridden locally via the + * {@link SpringExtensionConfig @SpringExtensionConfig} annotation or + * globally via the {@value #EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME} + * property. See the {@linkplain SpringExtension class-level Javadoc} for further + * details. * @since 7.0 * @see SpringExtensionConfig#useTestClassScopedExtensionContext() + * @see #EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME + * @see ExtensionContextScope */ @Override - public ExtensionContextScope getTestInstantiationExtensionContextScope(ExtensionContext rootContext) { - return ExtensionContextScope.TEST_METHOD; + public TestInstantiationAwareExtension.ExtensionContextScope getTestInstantiationExtensionContextScope( + ExtensionContext rootContext) { + + return TestInstantiationAwareExtension.ExtensionContextScope.TEST_METHOD; } /** @@ -464,20 +496,19 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes } /** - * Find the properly {@linkplain ExtensionContextScope scoped} {@link ExtensionContext} - * for the supplied test class. + * Find the properly scoped {@link ExtensionContext} for the supplied test class. *

If the supplied {@code ExtensionContext} is already properly scoped, it - * will be returned. Otherwise, if the test class is annotated with - * {@code @SpringExtensionConfig(useTestClassScopedExtensionContext = true)}, - * this method searches the {@code ExtensionContext} hierarchy for an - * {@code ExtensionContext} whose test class is the same as the supplied - * test class. + * will be returned. Otherwise, if test-class scoped semantics apply (see + * {@linkplain SpringExtension class-level Javadoc}), this method searches the + * {@code ExtensionContext} hierarchy for an {@code ExtensionContext} whose test + * class is the same as the supplied test class. * @since 7.0 * @see SpringExtensionConfig#useTestClassScopedExtensionContext() + * @see #EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME * @see ExtensionContextScope */ private static ExtensionContext findProperlyScopedExtensionContext(Class testClass, ExtensionContext context) { - if (useTestClassScopedExtensionContextCache.get(testClass)) { + if (shouldUseTestClassScopedExtensionContext(testClass, context)) { while (context.getRequiredTestClass() != testClass) { context = context.getParent().get(); } @@ -486,13 +517,11 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes } /** - * Determine if the supplied test class, or one of its enclosing classes, is annotated - * with {@code @SpringExtensionConfig(useTestClassScopedExtensionContext = true)}. + * Determine whether test-class scoped {@code ExtensionContext} semantics apply + * for the supplied test class. * @since 7.0 - * @see SpringExtensionConfig#useTestClassScopedExtensionContext() - * @see #useTestClassScopedExtensionContextCache */ - private static boolean useTestClassScopedExtensionContext(Class testClass) { + private static boolean shouldUseTestClassScopedExtensionContext(Class testClass, ExtensionContext context) { MergedAnnotation mergedAnnotation = MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY) .withEnclosingClasses(ClassUtils::isInnerClass) @@ -509,7 +538,96 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes return mergedAnnotation.getBoolean("useTestClassScopedExtensionContext"); } - return false; + return (resolveDefaultExtensionContextScope(context) == ExtensionContextScope.TEST_CLASS); + } + + /** + * Resolve the default {@link ExtensionContextScope} from the + * {@value #EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME} property, first via + * {@link SpringProperties} and then via + * {@link ExtensionContext#getConfigurationParameter(String)} as a fallback + * strategy if the Spring property is not set. + * @param context the current {@code ExtensionContext} + * @return the resolved scope, or {@link ExtensionContextScope#TEST_METHOD} + * if the property is not set + * @since 7.0.7 + */ + private static ExtensionContextScope resolveDefaultExtensionContextScope(ExtensionContext context) { + return context.getRoot().getStore(DEFAULT_EXTENSION_CONTEXT_SCOPE_NAMESPACE) + .computeIfAbsent(ExtensionContextScope.class, key -> { + String springValue = SpringProperties.getProperty(EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME); + ExtensionContextScope scope = ExtensionContextScope.from(springValue); + if (scope != null) { + return scope; + } + rejectUnsupportedScope(springValue); + + String junitValue = context.getConfigurationParameter(EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME) + .orElse(null); + scope = ExtensionContextScope.from(junitValue); + if (scope != null) { + return scope; + } + rejectUnsupportedScope(junitValue); + + // Default to test-method scope. + return ExtensionContextScope.TEST_METHOD; + }, ExtensionContextScope.class); + } + + private static void rejectUnsupportedScope(@Nullable String scope) { + if (StringUtils.hasText(scope)) { + throw new IllegalArgumentException("Unsupported value '%s' for property '%s'" + .formatted(scope.strip(), EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME)); + } + } + + + /** + * Enumeration of extension context scopes for configuring how the + * {@link SpringExtension} resolves an {@link ExtensionContext} within + * {@code @Nested} test class hierarchies. + * + * @since 7.0.7 + * @see SpringExtension#EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME + * @see SpringExtensionConfig + * @see org.junit.jupiter.api.extension.TestInstantiationAwareExtension.ExtensionContextScope + */ + public enum ExtensionContextScope { + + /** + * Use a test-method scoped {@link ExtensionContext} within {@code @Nested} + * test class hierarchies. + * @see org.junit.jupiter.api.extension.TestInstantiationAwareExtension.ExtensionContextScope#TEST_METHOD + */ + TEST_METHOD, + + /** + * Use a test-class scoped {@link ExtensionContext} within {@code @Nested} + * test class hierarchies. + * @see org.junit.jupiter.api.extension.TestInstantiationAwareExtension.ExtensionContextScope#DEFAULT + */ + TEST_CLASS; + + + /** + * Get the {@link ExtensionContextScope} enum constant with the supplied name, + * {@linkplain String#strip() stripped} and ignoring case. + * @param name the name of the enum constant to retrieve + * @return the corresponding enum constant, or {@code null} if not found + * @see ExtensionContextScope#valueOf(String) + */ + static @Nullable ExtensionContextScope from(@Nullable String name) { + if (!StringUtils.hasText(name)) { + return null; + } + try { + return ExtensionContextScope.valueOf(name.strip().toUpperCase(Locale.ROOT)); + } + catch (IllegalArgumentException ex) { + return null; + } + } } } diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtensionConfig.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtensionConfig.java index 6d7d58a5750..702586bb17d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtensionConfig.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtensionConfig.java @@ -65,8 +65,12 @@ public @interface SpringExtensionConfig { * will always use a test-class scoped {@code ExtensionContext}, and there is no need * to declare {@code @SpringExtensionConfig(useTestClassScopedExtensionContext = true)}. * + *

Furthermore, this attribute takes precedence over global configuration + * of the {@code spring.test.extension.context.scope} property. + * * @see SpringExtension - * @see SpringExtension#getTestInstantiationExtensionContextScope(org.junit.jupiter.api.extension.ExtensionContext) + * @see SpringExtension#EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME + * @see SpringExtension.ExtensionContextScope */ boolean useTestClassScopedExtensionContext(); diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/SpringExtensionExtensionContextScopeTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/SpringExtensionExtensionContextScopeTests.java new file mode 100644 index 00000000000..92de037a721 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/SpringExtensionExtensionContextScopeTests.java @@ -0,0 +1,208 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.junit.jupiter; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.platform.testkit.engine.EngineTestKit; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.SpringProperties; +import org.springframework.core.env.Environment; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension.ExtensionContextScope; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.OVERRIDE; +import static org.springframework.test.context.junit.jupiter.SpringExtension.EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME; +import static org.springframework.test.context.junit.jupiter.SpringExtension.ExtensionContextScope.TEST_CLASS; +import static org.springframework.test.context.junit.jupiter.SpringExtension.ExtensionContextScope.TEST_METHOD; + +/** + * Tests for {@link SpringExtension.ExtensionContextScope} and + * {@link SpringExtension#EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME}. + * + * @author Sam Brannen + * @since 7.0.7 + */ +class SpringExtensionExtensionContextScopeTests { + + @Test + void extensionContextScopeFromString() { + assertThat(ExtensionContextScope.from(null)).isNull(); + assertThat(ExtensionContextScope.from("")).isNull(); + assertThat(ExtensionContextScope.from(" ")).isNull(); + assertThat(ExtensionContextScope.from("bogus")).isNull(); + + assertThat(ExtensionContextScope.from("TEST_METHOD")).isSameAs(TEST_METHOD); + assertThat(ExtensionContextScope.from("test_method")).isSameAs(TEST_METHOD); + assertThat(ExtensionContextScope.from("Test_Method")).isSameAs(TEST_METHOD); + + assertThat(ExtensionContextScope.from("TEST_CLASS")).isSameAs(TEST_CLASS); + assertThat(ExtensionContextScope.from("test_class")).isSameAs(TEST_CLASS); + assertThat(ExtensionContextScope.from("Test_Class")).isSameAs(TEST_CLASS); + } + + @Test + void invalidExtensionContextScopeIsRejectedWhenConfiguredViaSpringProperties() { + SpringProperties.setProperty(EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME, "bogus"); + try { + EngineTestKit.engine("junit-jupiter") + .selectors(selectClass(InvalidScopeTestCase.class)) + .execute() + .testEvents() + .assertStatistics(stats -> stats.started(1).failed(1)); + } + finally { + SpringProperties.setProperty(EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME, null); + } + } + + @Test + void invalidExtensionContextScopeIsRejectedWhenConfiguredViaJUnitConfigurationParameter() { + EngineTestKit.engine("junit-jupiter") + .selectors(selectClass(InvalidScopeTestCase.class)) + .configurationParameter(EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME, "bogus") + .execute() + .testEvents() + .assertStatistics(stats -> stats.started(1).failed(1)); + } + + @Test + void testClassScopeConfiguredViaSpringProperties() { + SpringProperties.setProperty(EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME, TEST_CLASS.name()); + try { + var results = EngineTestKit.engine("junit-jupiter") + .selectors(selectClass(GlobalClassScopedConfigurationTestCase.class)) + .execute(); + results.containerEvents() + .assertStatistics(stats -> stats.started(3).succeeded(3).failed(0)); + results.testEvents() + .assertStatistics(stats -> stats.started(2).succeeded(2).failed(0)); + } + finally { + SpringProperties.setProperty(EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME, null); + } + } + + @Test + void testClassScopeConfiguredViaJUnitConfigurationParameter() { + var results = EngineTestKit.engine("junit-jupiter") + .selectors(selectClass(GlobalClassScopedConfigurationTestCase.class)) + .configurationParameter(EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME, TEST_CLASS.name()) + .execute(); + results.containerEvents() + .assertStatistics(stats -> stats.started(3).succeeded(3).failed(0)); + results.testEvents() + .assertStatistics(stats -> stats.started(2).succeeded(2).failed(0)); + } + + @Test + void springExtensionConfigOverridesGlobalTestClassScopeConfiguration() { + EngineTestKit.engine("junit-jupiter") + .selectors(selectClass(SpringExtensionConfigOverridesGlobalPropertyTestCase.class)) + .configurationParameter(EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME, TEST_CLASS.name()) + .execute() + .testEvents() + .assertStatistics(stats -> stats.started(2).succeeded(2).failed(0)); + } + + + @SpringJUnitConfig + static class InvalidScopeTestCase { + + @Test + void test() { + // no-op + } + } + + @SpringJUnitConfig + @TestPropertySource(properties = "p1 = v1") + @NestedTestConfiguration(OVERRIDE) + @FailingTestCase + static class GlobalClassScopedConfigurationTestCase { + + @Autowired + Environment env1; + + @Test + void propertiesInEnvironment() { + assertThat(env1.getProperty("p1")).isEqualTo("v1"); + } + + @Nested + @SpringJUnitConfig(Config.class) + @TestPropertySource(properties = "p2 = v2") + class ConfigOverriddenByDefaultTests { + + @Autowired + Environment env2; + + @Test + void propertiesInEnvironment() { + assertThat(env1.getProperty("p1")).isEqualTo("v1"); + assertThat(env1).isNotSameAs(env2); + assertThat(env2.getProperty("p1")).isNull(); + assertThat(env2.getProperty("p2")).isEqualTo("v2"); + } + } + + @Configuration + static class Config { + } + } + + @SpringJUnitConfig + @SpringExtensionConfig(useTestClassScopedExtensionContext = false) + @TestPropertySource(properties = "p1 = v1") + @NestedTestConfiguration(OVERRIDE) + static class SpringExtensionConfigOverridesGlobalPropertyTestCase { + + @Autowired + Environment env1; + + @Test + void propertiesInEnvironment() { + assertThat(env1.getProperty("p1")).isEqualTo("v1"); + } + + @Nested + @SpringJUnitConfig(Config.class) + @TestPropertySource(properties = "p2 = v2") + class ConfigOverriddenByDefaultTests { + + @Autowired + Environment env2; + + @Test + void propertiesInEnvironment() { + assertThat(env1).isSameAs(env2); + assertThat(env2.getProperty("p1")).isNull(); + assertThat(env2.getProperty("p2")).isEqualTo("v2"); + } + } + + @Configuration + static class Config { + } + } + +}