mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Use test-method scoped ExtensionContext in the SpringExtension
As of Spring Framework 6.2.13, we support JUnit Jupiter 5.12's ExtensionContextScope.TEST_METHOD behavior in the SpringExtension and the BeanOverrideTestExecutionListener; however, users can only benefit from that if they explicitly set the following configuration parameter for their entire test suite, which may have adverse effects on other third-party JUnit Jupiter extensions. junit.jupiter.extensions.testinstantiation.extensioncontextscope.default=test_method For Spring Framework 7.0, in order to support dependency injection into test class constructors and fields in @Nested test class hierarchies from the same ApplicationContext that is already used to perform dependency injection into lifecycle and test methods (@BeforeEach, @AfterEach, @Test, etc.), we have decided to configure the SpringExtension to use ExtensionContextScope.TEST_METHOD by default. In addition, we have decided to provide a mechanism for users to switch back to the legacy "test-class scoped ExtensionContext" behavior in case third-party TestExecutionListener implementations are not yet compatible with test-method scoped ExtensionContext and TestContext semantics. This commit achieves the above goals as follows. - A new @SpringExtensionConfig annotation has been introduced, which allows developers to configure the effective ExtensionContext scope used by the SpringExtension. - The SpringExtension now overrides getTestInstantiationExtensionContextScope() to return ExtensionContextScope.TEST_METHOD. - The postProcessTestInstance() and resolveParameter() methods in the SpringExtension now find the properly scoped ExtensionContext for the supplied test class, based on whether the @Nested test class hierarchy is annotated with @SpringExtensionConfig(useTestClassScopedExtensionContext = true). See gh-35680 See gh-35716 Closes gh-35697
This commit is contained in:
@@ -80,6 +80,8 @@ import org.springframework.core.annotation.AliasFor;
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 2.5
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringExtensionConfig @SpringExtensionConfig
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringJUnitConfig @SpringJUnitConfig
|
||||
* @see org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig @SpringJUnitWebConfig
|
||||
* @see ContextHierarchy @ContextHierarchy
|
||||
|
||||
+3
@@ -95,6 +95,9 @@ import org.jspecify.annotations.Nullable;
|
||||
* <li>{@link TestConstructor @TestConstructor}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Note that {@code @NestedTestConfiguration} does not apply to
|
||||
* {@link org.springframework.test.context.junit.jupiter.SpringExtensionConfig @SpringExtensionConfig}.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 5.3
|
||||
* @see EnclosingConfiguration#INHERIT
|
||||
|
||||
@@ -61,6 +61,7 @@ import org.jspecify.annotations.Nullable;
|
||||
* @see org.springframework.beans.factory.annotation.Autowired @Autowired
|
||||
* @see jakarta.inject.Inject @jakarta.inject.Inject
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringExtensionConfig @SpringExtensionConfig
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringJUnitConfig @SpringJUnitConfig
|
||||
* @see org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig @SpringJUnitWebConfig
|
||||
* @see ContextConfiguration @ContextConfiguration
|
||||
|
||||
@@ -117,6 +117,7 @@ public interface TestContext extends AttributeAccessor, Serializable {
|
||||
* invoke {@code testContext.getTestInstance().getClass()} instead of
|
||||
* {@code testContext.getTestClass()}.
|
||||
* @return the test class (never {@code null})
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringExtensionConfig @SpringExtensionConfig
|
||||
*/
|
||||
Class<?> getTestClass();
|
||||
|
||||
|
||||
@@ -169,6 +169,7 @@ public interface TestExecutionListener {
|
||||
* concrete classes as necessary.
|
||||
* @param testContext the test context for the test; never {@code null}
|
||||
* @throws Exception allows any exception to propagate
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringExtensionConfig @SpringExtensionConfig
|
||||
*/
|
||||
default void prepareTestInstance(TestContext testContext) throws Exception {
|
||||
}
|
||||
|
||||
+108
-8
@@ -48,6 +48,7 @@ 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.annotation.MergedAnnotation;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import org.springframework.core.annotation.RepeatableContainers;
|
||||
@@ -59,6 +60,8 @@ import org.springframework.test.context.event.RecordApplicationEvents;
|
||||
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;
|
||||
|
||||
@@ -66,18 +69,37 @@ import org.springframework.util.ReflectionUtils.MethodFilter;
|
||||
* {@code SpringExtension} integrates the <em>Spring TestContext Framework</em>
|
||||
* into the JUnit Jupiter testing framework.
|
||||
*
|
||||
* <p>To use this extension, simply annotate a JUnit Jupiter based test class with
|
||||
* {@code @ExtendWith(SpringExtension.class)}, {@code @SpringJUnitConfig}, or
|
||||
* {@code @SpringJUnitWebConfig}.
|
||||
* <p>To use this extension, annotate a JUnit Jupiter based test class with
|
||||
* {@code @ExtendWith(SpringExtension.class)}, {@code @SpringJUnitConfig},
|
||||
* {@code @SpringJUnitWebConfig}, or any other annotation that is meta-annotated
|
||||
* with {@code @ExtendWith(SpringExtension.class)} such as {@code @SpringBootTest},
|
||||
* etc.
|
||||
*
|
||||
* <p>As of Spring Framework 7.0, the {@code SpringExtension} is
|
||||
* {@linkplain #getTestInstantiationExtensionContextScope(ExtensionContext)
|
||||
* configured} to use a test-method scoped {@link ExtensionContext}, which
|
||||
* enables consistent dependency injection into fields and constructors from the
|
||||
* {@link ApplicationContext} for the current test method in a
|
||||
* {@link org.junit.jupiter.api.Nested @Nested} test class hierarchy. However,
|
||||
* if a third-party {@link org.junit.platform.launcher.TestExecutionListener
|
||||
* 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
|
||||
* {@link SpringExtensionConfig#useTestClassScopedExtensionContext()
|
||||
* @SpringExtensionConfig(useTestClassScopedExtensionContext = true)}.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @author Simon Baslé
|
||||
* @since 5.0
|
||||
* @see org.springframework.test.context.junit.jupiter.EnabledIf
|
||||
* @see org.springframework.test.context.junit.jupiter.DisabledIf
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringJUnitConfig
|
||||
* @see org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig
|
||||
* @see org.springframework.test.context.TestContextManager
|
||||
* @see org.junit.jupiter.api.extension.ExtendWith @ExtendWith
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringExtensionConfig @SpringExtensionConfig
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringJUnitConfig @SpringJUnitConfig
|
||||
* @see org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig @SpringJUnitWebConfig
|
||||
* @see org.springframework.test.context.junit.jupiter.EnabledIf @EnabledIf
|
||||
* @see org.springframework.test.context.junit.jupiter.DisabledIf @DisabledIf
|
||||
* @see org.springframework.test.context.TestContextManager TestContextManager
|
||||
*/
|
||||
public class SpringExtension implements BeforeAllCallback, AfterAllCallback, TestInstancePostProcessor,
|
||||
BeforeEachCallback, AfterEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback,
|
||||
@@ -110,6 +132,14 @@ 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<Class<?>, 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<Class<? extends Annotation>> JUPITER_ANNOTATION_TYPES =
|
||||
@@ -119,6 +149,19 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
|
||||
ReflectionUtils.USER_DECLARED_METHODS.and(SpringExtension::isAutowiredTestOrLifecycleMethod);
|
||||
|
||||
|
||||
/**
|
||||
* Returns {@link ExtensionContextScope#TEST_METHOD ExtensionContextScope.TEST_METHOD}.
|
||||
* <p>This can be effectively overridden by annotating a test class with
|
||||
* {@code @SpringExtensionConfig(useTestClassScopedExtensionContext = true)}.
|
||||
* See the {@link SpringExtension class-level Javadoc} for further details.
|
||||
* @since 7.0
|
||||
* @see SpringExtensionConfig#useTestClassScopedExtensionContext()
|
||||
*/
|
||||
@Override
|
||||
public ExtensionContextScope getTestInstantiationExtensionContextScope(ExtensionContext rootContext) {
|
||||
return ExtensionContextScope.TEST_METHOD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link TestContextManager#beforeTestClass}.
|
||||
*/
|
||||
@@ -151,6 +194,8 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
|
||||
*/
|
||||
@Override
|
||||
public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception {
|
||||
context = findProperlyScopedExtensionContext(testInstance.getClass(), context);
|
||||
|
||||
validateAutowiredConfig(context);
|
||||
validateRecordApplicationEventsConfig(context);
|
||||
TestContextManager testContextManager = getTestContextManager(context);
|
||||
@@ -332,7 +377,13 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
|
||||
public @Nullable Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
|
||||
Parameter parameter = parameterContext.getParameter();
|
||||
int index = parameterContext.getIndex();
|
||||
Executable executable = parameterContext.getDeclaringExecutable();
|
||||
Class<?> testClass = extensionContext.getRequiredTestClass();
|
||||
if (executable instanceof Constructor<?> constructor) {
|
||||
testClass = constructor.getDeclaringClass();
|
||||
extensionContext = findProperlyScopedExtensionContext(testClass, extensionContext);
|
||||
}
|
||||
|
||||
ApplicationContext applicationContext = getApplicationContext(extensionContext);
|
||||
return ParameterResolutionDelegate.resolveDependency(parameter, index, testClass,
|
||||
applicationContext.getAutowireCapableBeanFactory());
|
||||
@@ -390,4 +441,53 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the properly {@linkplain ExtensionContextScope scoped} {@link ExtensionContext}
|
||||
* for the supplied test class.
|
||||
* <p>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.
|
||||
* @since 7.0
|
||||
* @see SpringExtensionConfig#useTestClassScopedExtensionContext()
|
||||
* @see ExtensionContextScope
|
||||
*/
|
||||
private static ExtensionContext findProperlyScopedExtensionContext(Class<?> testClass, ExtensionContext context) {
|
||||
if (useTestClassScopedExtensionContextCache.get(testClass)) {
|
||||
while (context.getRequiredTestClass() != testClass) {
|
||||
context = context.getParent().get();
|
||||
}
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the supplied test class, or one of its enclosing classes, is annotated
|
||||
* with {@code @SpringExtensionConfig(useTestClassScopedExtensionContext = true)}.
|
||||
* @since 7.0
|
||||
* @see SpringExtensionConfig#useTestClassScopedExtensionContext()
|
||||
* @see #useTestClassScopedExtensionContextCache
|
||||
*/
|
||||
private static boolean useTestClassScopedExtensionContext(Class<?> testClass) {
|
||||
MergedAnnotation<SpringExtensionConfig> mergedAnnotation =
|
||||
MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY)
|
||||
.withEnclosingClasses(ClassUtils::isInnerClass)
|
||||
.from(testClass)
|
||||
.get(SpringExtensionConfig.class);
|
||||
|
||||
if (mergedAnnotation.isPresent()) {
|
||||
if (mergedAnnotation.getSource() instanceof Class<?> source && ClassUtils.isInnerClass(source)) {
|
||||
throw new IllegalStateException("""
|
||||
Test class [%s] must not be annotated with @SpringExtensionConfig. \
|
||||
@SpringExtensionConfig is only supported on top-level classes.\
|
||||
""".formatted(source.getName()));
|
||||
}
|
||||
return mergedAnnotation.getBoolean("useTestClassScopedExtensionContext");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 java.lang.annotation.Documented;
|
||||
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;
|
||||
|
||||
/**
|
||||
* {@code @SpringExtensionConfig} is a type-level annotation that can be used to
|
||||
* configure the behavior of the {@link SpringExtension}.
|
||||
*
|
||||
* <p>This annotation is only applicable to {@link org.junit.jupiter.api.Nested @Nested}
|
||||
* test class hierarchies and should be applied to the top-level enclosing class
|
||||
* of a {@code @Nested} test class hierarchy. Consequently, there is no need to
|
||||
* declare this annotation on a test class that does not contain {@code @Nested}
|
||||
* test classes.
|
||||
*
|
||||
* <p>Note that
|
||||
* {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
|
||||
* does not apply to this annotation: {@code @SpringExtensionConfig} will always be
|
||||
* detected within a {@code @Nested} test class hierarchy, effectively disregarding
|
||||
* any {@code @NestedTestConfiguration(OVERRIDE)} declarations.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 7.0
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringJUnitConfig @SpringJUnitConfig
|
||||
* @see org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig @SpringJUnitWebConfig
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Inherited
|
||||
public @interface SpringExtensionConfig {
|
||||
|
||||
/**
|
||||
* Specify whether the {@link SpringExtension} should use a test-class scoped
|
||||
* {@link org.junit.jupiter.api.extension.ExtensionContext ExtensionContext}
|
||||
* within {@link org.junit.jupiter.api.Nested @Nested} test class hierarchies.
|
||||
*
|
||||
* <p>By default, the {@code SpringExtension} uses a test-method scoped
|
||||
* {@code ExtensionContext}. Thus, there is no need to declare this annotation
|
||||
* attribute with a value of {@code false}.
|
||||
*
|
||||
* @see SpringExtension
|
||||
* @see SpringExtension#getTestInstantiationExtensionContextScope(org.junit.jupiter.api.extension.ExtensionContext)
|
||||
*/
|
||||
boolean useTestClassScopedExtensionContext();
|
||||
|
||||
}
|
||||
+5
-4
@@ -42,10 +42,11 @@ import org.springframework.test.context.ContextLoader;
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 5.0
|
||||
* @see ExtendWith
|
||||
* @see SpringExtension
|
||||
* @see ContextConfiguration
|
||||
* @see org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig
|
||||
* @see org.junit.jupiter.api.extension.ExtendWith @ExtendWith
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringExtensionConfig @SpringExtensionConfig
|
||||
* @see org.springframework.test.context.junit.ContextConfiguration @ContextConfiguration
|
||||
* @see org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig @SpringJUnitWebConfig
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration
|
||||
|
||||
+6
-5
@@ -45,11 +45,12 @@ import org.springframework.test.context.web.WebAppConfiguration;
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 5.0
|
||||
* @see ExtendWith
|
||||
* @see SpringExtension
|
||||
* @see ContextConfiguration
|
||||
* @see WebAppConfiguration
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringJUnitConfig
|
||||
* @see org.junit.jupiter.api.extension.ExtendWith @ExtendWith
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringExtensionConfig @SpringExtensionConfig
|
||||
* @see org.springframework.test.context.junit.ContextConfiguration @ContextConfiguration
|
||||
* @see org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration
|
||||
* @see org.springframework.test.context.junit.jupiter.SpringJUnitConfig @SpringJUnitConfig
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration
|
||||
|
||||
Reference in New Issue
Block a user