Skip to content

Commit a281bdb

Browse files
committed
Introduce context bootstrap strategy in the TCF
Work done in conjunction with SPR-5243 and SPR-4588 introduced physical package cycles in the spring-test module. The work performed in conjunction with SPR-9924 uses reflection to resolve these physical package cycles; however, prior to this commit the logical package cycles still remain. Furthermore, over time it has become apparent that the Spring TestContext Framework (TCF) could better serve application developers and especially third-party framework developers by providing a more flexible mechanism for "bootstrapping" the TCF. For example, prior to this commit, default TestExecutionListeners could only be registered by subclassing TestContextManager (and SpringJUnit4ClassRunner if using JUnit). Similarly, the default ContextLoader could only be set by subclassing SpringJUnit4ClassRunner for JUnit and by copying and modifying AbstractTestNGSpringContextTests for TestNG. This commit addresses the aforementioned issues by introducing a bootstrap strategy in the TestContext framework that is responsible for determining default TestExecutionListeners and the default ContextLoader in an extensible fashion. The new TestContextBootstrapper SPI also provides a mechanism for supporting various types of MergedContextConfiguration depending on the feature set of the context loaders supported by the strategy. The following provides an overview of the most significant changes in this commit. - Introduced TestContextBootstrapper strategy SPI, BootstrapContext, and @BootstrapWith. - Introduced AbstractTestContextBootstrapper, DefaultTestContextBootstrapper, and WebTestContextBootstrapper implementations of the TestContextBootstrapper SPI and extracted related reflection code from ContextLoaderUtils & TestContextManager. - Introduced BootstrapUtils for retrieving the TestContextBootstrapper from @BootstrapWith, falling back to a default if @BootstrapWith is not present. - @WebAppConfiguration is now annotated with @BootstrapWith(WebTestContextBootstrapper.class). - CacheAwareContextLoaderDelegate is now an interface with a new DefaultCacheAwareContextLoaderDelegate implementation class. - Introduced closeContext(MergedContextConfiguration, HierarchyMode) in CacheAwareContextLoaderDelegate. - DefaultTestContext now uses CacheAwareContextLoaderDelegate instead of interacting directly with the ContextCache. - DefaultTestContext now delegates to a TestContextBootstrapper for building the MergedContextConfiguration. - TestContextManager now delegates to TestContextBootstrapper for retrieving TestExecutionListeners. - Deleted TestContextManager(Class, String) constructor and SpringJUnit4ClassRunner.getDefaultContextLoaderClassName(Class) method since default ContextLoader support is now implemented by TestContextBootstrappers. - Extracted ActiveProfilesUtils from ContextLoaderUtils. - Extracted ApplicationContextInitializerUtils from ContextLoaderUtils. - MetaAnnotationUtils is now a public utility class in the test.util package. - Removed restriction in @activeprofiles that a custom resolver cannot be used with the 'value' or 'profiles' attributes. - Introduced DefaultActiveProfilesResolver. Issue: SPR-9955
1 parent 196cdef commit a281bdb

37 files changed

+2194
-1265
lines changed

spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -50,29 +50,23 @@
5050
/**
5151
* Alias for {@link #profiles}.
5252
*
53-
* <p>This attribute may <strong>not</strong> be used in conjunction
54-
* with {@link #profiles} or {@link #resolver}, but it may be used
55-
* <em>instead</em> of them.
53+
* <p>This attribute may <strong>not</strong> be used in conjunction with
54+
* {@link #profiles}, but it may be used <em>instead</em> of {@link #profiles}.
5655
*/
5756
String[] value() default {};
5857

5958
/**
6059
* The bean definition profiles to activate.
6160
*
62-
* <p>This attribute may <strong>not</strong> be used in conjunction
63-
* with {@link #value} or {@link #resolver}, but it may be used
64-
* <em>instead</em> of them.
61+
* <p>This attribute may <strong>not</strong> be used in conjunction with
62+
* {@link #value}, but it may be used <em>instead</em> of {@link #value}.
6563
*/
6664
String[] profiles() default {};
6765

6866
/**
6967
* The type of {@link ActiveProfilesResolver} to use for resolving the active
7068
* bean definition profiles programmatically.
7169
*
72-
* <p>This attribute may <strong>not</strong> be used in conjunction
73-
* with {@link #profiles} or {@link #value}, but it may be used <em>instead</em>
74-
* of them in order to resolve the active profiles programmatically.
75-
*
7670
* @since 4.0
7771
* @see ActiveProfilesResolver
7872
*/
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2002-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context;
18+
19+
/**
20+
* {@code BootstrapContext} encapsulates the context in which the <em>Spring
21+
* TestContext Framework</em> is bootstrapped.
22+
*
23+
* @author Sam Brannen
24+
* @since 4.1
25+
* @see BootstrapWith
26+
* @see TestContextBootstrapper
27+
*/
28+
public interface BootstrapContext {
29+
30+
/**
31+
* Get the {@link Class test class} for this bootstrap context.
32+
* @return the test class (never {@code null})
33+
*/
34+
Class<?> getTestClass();
35+
36+
/**
37+
* Get the {@link CacheAwareContextLoaderDelegate} to use for transparent
38+
* interaction with the <em>context cache</em>.
39+
*/
40+
CacheAwareContextLoaderDelegate getCacheAwareContextLoaderDelegate();
41+
42+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2002-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context;
18+
19+
import org.apache.commons.logging.Log;
20+
import org.apache.commons.logging.LogFactory;
21+
import org.springframework.util.ClassUtils;
22+
23+
import static org.springframework.beans.BeanUtils.*;
24+
import static org.springframework.core.annotation.AnnotationUtils.*;
25+
26+
/**
27+
* {@code BootstrapUtils} is a collection of utility methods to assist with
28+
* bootstrapping the <em>Spring TestContext Framework</em>.
29+
*
30+
* @author Sam Brannen
31+
* @since 4.1
32+
* @see BootstrapWith
33+
* @see BootstrapContext
34+
* @see TestContextBootstrapper
35+
*/
36+
abstract class BootstrapUtils {
37+
38+
private static final String DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME = "org.springframework.test.context.support.DefaultTestContextBootstrapper";
39+
40+
private static final Log logger = LogFactory.getLog(BootstrapUtils.class);
41+
42+
43+
private BootstrapUtils() {
44+
/* no-op */
45+
}
46+
47+
/**
48+
* Resolve the {@link TestContextBootstrapper} type for the test class in the
49+
* supplied {@link BootstrapContext}, instantiate it, and provide it a reference
50+
* to the {@link BootstrapContext}.
51+
*
52+
* <p>If the {@link BootstrapWith @BootstrapWith} annotation is present on
53+
* the test class, either directly or as a meta-annotation, then its
54+
* {@link BootstrapWith#value value} will be used as the bootstrapper type.
55+
* Otherwise, the {@link org.springframework.test.context.support.DefaultTestContextBootstrapper
56+
* DefaultTestContextBootstrapper} will be used.
57+
*
58+
* @param bootstrapContext the bootstrap context to use
59+
* @return a fully configured {@code TestContextBootstrapper}
60+
*/
61+
@SuppressWarnings("unchecked")
62+
static TestContextBootstrapper resolveTestContextBootstrapper(BootstrapContext bootstrapContext) {
63+
Class<?> testClass = bootstrapContext.getTestClass();
64+
65+
Class<? extends TestContextBootstrapper> clazz = null;
66+
try {
67+
BootstrapWith bootstrapWith = findAnnotation(testClass, BootstrapWith.class);
68+
if (bootstrapWith != null && !TestContextBootstrapper.class.equals(bootstrapWith.value())) {
69+
clazz = bootstrapWith.value();
70+
}
71+
else {
72+
clazz = (Class<? extends TestContextBootstrapper>) ClassUtils.forName(
73+
DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, BootstrapUtils.class.getClassLoader());
74+
}
75+
76+
if (logger.isDebugEnabled()) {
77+
logger.debug(String.format("Instantiating TestContextBootstrapper from class [%s]", clazz.getName()));
78+
}
79+
80+
TestContextBootstrapper testContextBootstrapper = instantiateClass(clazz, TestContextBootstrapper.class);
81+
testContextBootstrapper.setBootstrapContext(bootstrapContext);
82+
83+
return testContextBootstrapper;
84+
}
85+
catch (Throwable t) {
86+
throw new IllegalStateException("Could not load TestContextBootstrapper [" + clazz
87+
+ "]. Specify @BootstrapWith's 'value' attribute "
88+
+ "or make the default bootstrapper class available.", t);
89+
}
90+
}
91+
92+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2002-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Inherited;
22+
import java.lang.annotation.Retention;
23+
import java.lang.annotation.RetentionPolicy;
24+
import java.lang.annotation.Target;
25+
26+
/**
27+
* {@code @BootstrapWith} defines class-level metadata that is used to determine
28+
* how to bootstrap the <em>Spring TestContext Framework</em>.
29+
*
30+
* <p>This annotation may also be used as a <em>meta-annotation</em> to create
31+
* custom <em>composed annotations</em>.
32+
*
33+
* @author Sam Brannen
34+
* @since 4.1
35+
* @see BootstrapContext
36+
* @see TestContextBootstrapper
37+
*/
38+
@Documented
39+
@Inherited
40+
@Retention(RetentionPolicy.RUNTIME)
41+
@Target(ElementType.TYPE)
42+
public @interface BootstrapWith {
43+
44+
/**
45+
* The {@link TestContextBootstrapper} to use to bootstrap the <em>Spring
46+
* TestContext Framework</em>.
47+
*/
48+
Class<? extends TestContextBootstrapper> value() default TestContextBootstrapper.class;
49+
50+
}
Lines changed: 41 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,97 +16,61 @@
1616

1717
package org.springframework.test.context;
1818

19-
import org.apache.commons.logging.Log;
20-
import org.apache.commons.logging.LogFactory;
2119
import org.springframework.context.ApplicationContext;
22-
import org.springframework.util.Assert;
20+
import org.springframework.context.ConfigurableApplicationContext;
21+
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
2322

2423
/**
25-
* {@code CacheAwareContextLoaderDelegate} loads application contexts from
26-
* {@link MergedContextConfiguration} by delegating to the
27-
* {@link ContextLoader} configured in the {@code MergedContextConfiguration}
28-
* and interacting transparently with the {@link ContextCache} behind the scenes.
24+
* A {@code CacheAwareContextLoaderDelegate} is responsible for {@linkplain
25+
* #loadContext loading} and {@linkplain #closeContext closing} application
26+
* contexts, interacting transparently with a <em>context cache</em> behind
27+
* the scenes.
2928
*
30-
* <p>Note: {@code CacheAwareContextLoaderDelegate} does not implement the
29+
* <p>Note: {@code CacheAwareContextLoaderDelegate} does not extend the
3130
* {@link ContextLoader} or {@link SmartContextLoader} interface.
3231
*
3332
* @author Sam Brannen
3433
* @since 3.2.2
3534
*/
36-
public class CacheAwareContextLoaderDelegate {
37-
38-
private static final Log logger = LogFactory.getLog(CacheAwareContextLoaderDelegate.class);
39-
40-
private final ContextCache contextCache;
41-
42-
43-
CacheAwareContextLoaderDelegate(ContextCache contextCache) {
44-
Assert.notNull(contextCache, "ContextCache must not be null");
45-
this.contextCache = contextCache;
46-
}
35+
public interface CacheAwareContextLoaderDelegate {
4736

4837
/**
49-
* Load the {@code ApplicationContext} for the supplied merged context
50-
* configuration. Supports both the {@link SmartContextLoader} and
51-
* {@link ContextLoader} SPIs.
52-
* @throws Exception if an error occurs while loading the application context
53-
*/
54-
private ApplicationContext loadContextInternal(MergedContextConfiguration mergedContextConfiguration)
55-
throws Exception {
56-
ContextLoader contextLoader = mergedContextConfiguration.getContextLoader();
57-
Assert.notNull(contextLoader, "Cannot load an ApplicationContext with a NULL 'contextLoader'. "
58-
+ "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy.");
59-
60-
ApplicationContext applicationContext;
61-
62-
if (contextLoader instanceof SmartContextLoader) {
63-
SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader;
64-
applicationContext = smartContextLoader.loadContext(mergedContextConfiguration);
65-
}
66-
else {
67-
String[] locations = mergedContextConfiguration.getLocations();
68-
Assert.notNull(locations, "Cannot load an ApplicationContext with a NULL 'locations' array. "
69-
+ "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy.");
70-
applicationContext = contextLoader.loadContext(locations);
71-
}
72-
73-
return applicationContext;
74-
}
75-
76-
/**
77-
* Load the {@link ApplicationContext application context} for the supplied
78-
* merged context configuration.
38+
* Load the {@linkplain ApplicationContext application context} for the supplied
39+
* {@link MergedContextConfiguration} by delegating to the {@link ContextLoader}
40+
* configured in the given {@code MergedContextConfiguration}.
41+
*
42+
* <p>If the context is present in the <em>context cache</em> it will simply
43+
* be returned; otherwise, it will be loaded, stored in the cache, and returned.
7944
*
80-
* <p>If the context is present in the cache it will simply be returned;
81-
* otherwise, it will be loaded, stored in the cache, and returned.
45+
* @param mergedContextConfiguration the merged context configuration to use
46+
* to load the application context; never {@code null}
8247
* @return the application context
8348
* @throws IllegalStateException if an error occurs while retrieving or
8449
* loading the application context
8550
*/
86-
public ApplicationContext loadContext(MergedContextConfiguration mergedContextConfiguration) {
87-
synchronized (contextCache) {
88-
ApplicationContext context = contextCache.get(mergedContextConfiguration);
89-
if (context == null) {
90-
try {
91-
context = loadContextInternal(mergedContextConfiguration);
92-
if (logger.isDebugEnabled()) {
93-
logger.debug(String.format("Storing ApplicationContext in cache under key [%s].",
94-
mergedContextConfiguration));
95-
}
96-
contextCache.put(mergedContextConfiguration, context);
97-
}
98-
catch (Exception ex) {
99-
throw new IllegalStateException("Failed to load ApplicationContext", ex);
100-
}
101-
}
102-
else {
103-
if (logger.isDebugEnabled()) {
104-
logger.debug(String.format("Retrieved ApplicationContext from cache with key [%s].",
105-
mergedContextConfiguration));
106-
}
107-
}
108-
return context;
109-
}
110-
}
51+
ApplicationContext loadContext(MergedContextConfiguration mergedContextConfiguration);
52+
53+
/**
54+
* Remove the {@linkplain ApplicationContext application context} for the
55+
* supplied {@link MergedContextConfiguration} from the <em>context cache</em>
56+
* and {@linkplain ConfigurableApplicationContext#close() close} it if it is
57+
* an instance of {@link ConfigurableApplicationContext}.
58+
*
59+
* <p>The semantics of the supplied {@code HierarchyMode} must be honored when
60+
* removing the context from the cache. See the Javadoc for {@link HierarchyMode}
61+
* for details.
62+
*
63+
* <p>Generally speaking, this method should only be called if the state of
64+
* a singleton bean has been changed (potentially affecting future interaction
65+
* with the context) or if the context needs to be prematurely removed from
66+
* the cache.
67+
*
68+
* @param mergedContextConfiguration the merged context configuration for the
69+
* application context to close; never {@code null}
70+
* @param hierarchyMode the hierarchy mode; may be {@code null} if the context
71+
* is not part of a hierarchy
72+
* @since 4.1
73+
*/
74+
void closeContext(MergedContextConfiguration mergedContextConfiguration, HierarchyMode hierarchyMode);
11175

11276
}

spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
*
9292
* <p>This attribute may <strong>not</strong> be used in conjunction with
9393
* {@link #locations}, but it may be used instead of {@link #locations}.
94+
*
9495
* @since 3.0
9596
* @see #inheritLocations
9697
*/
@@ -120,6 +121,7 @@
120121
*
121122
* <p>This attribute may <strong>not</strong> be used in conjunction with
122123
* {@link #value}, but it may be used instead of {@link #value}.
124+
*
123125
* @since 2.5
124126
* @see #inheritLocations
125127
*/

0 commit comments

Comments
 (0)