Skip to content

Commit 2244461

Browse files
philwebbsbrannen
authored andcommitted
Allow @ContextConfiguration to be omitted
Prior to this commit, the @ContextConfiguration annotation was required to be present even if default XML files, Groovy scripts, or @configuration classes were detected; however, in such cases the @ContextConfiguration was typically declared empty and therefore seemingly unnecessary boilerplate. This commit permits @ContextConfiguration to be omitted whenever it can be reasonably deduced. Consequently, integration tests such as the following are now supported. @RunWith(SpringRunner.class) public class MyTest { @Autowired String myBean; @test public void example() { /* ... */ } @configuration static class Config { @bean String myBean() { return "Hello"; } } } Issue: SPR-13955
1 parent 0a56667 commit 2244461

File tree

8 files changed

+220
-93
lines changed

8 files changed

+220
-93
lines changed

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 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.
@@ -34,6 +34,7 @@
3434
* attributes declared via {@link ContextConfiguration @ContextConfiguration}.
3535
*
3636
* @author Sam Brannen
37+
* @author Phillip Webb
3738
* @since 3.1
3839
* @see ContextConfiguration
3940
* @see SmartContextLoader#processContextConfiguration(ContextConfigurationAttributes)
@@ -43,6 +44,10 @@ public class ContextConfigurationAttributes {
4344

4445
private static final Log logger = LogFactory.getLog(ContextConfigurationAttributes.class);
4546

47+
private static final String[] EMPTY_LOCATIONS = new String[0];
48+
49+
private static final Class<?>[] EMPTY_CLASSES = new Class<?>[0];
50+
4651
private final Class<?> declaringClass;
4752

4853
private Class<?>[] classes;
@@ -60,6 +65,18 @@ public class ContextConfigurationAttributes {
6065
private final Class<? extends ContextLoader> contextLoaderClass;
6166

6267

68+
/**
69+
* Construct a new {@link ContextConfigurationAttributes} instance with default
70+
* values.
71+
* @param declaringClass the test class that declared {@code @ContextConfiguration},
72+
* either explicitly or implicitly
73+
* @since 4.3
74+
*/
75+
@SuppressWarnings("unchecked")
76+
public ContextConfigurationAttributes(Class<?> declaringClass) {
77+
this(declaringClass, EMPTY_LOCATIONS, EMPTY_CLASSES, false, (Class[]) EMPTY_CLASSES, true, ContextLoader.class);
78+
}
79+
6380
/**
6481
* Construct a new {@link ContextConfigurationAttributes} instance for the
6582
* supplied {@link ContextConfiguration @ContextConfiguration} annotation and
@@ -158,7 +175,8 @@ public ContextConfigurationAttributes(
158175

159176
/**
160177
* Get the {@linkplain Class class} that declared the
161-
* {@link ContextConfiguration @ContextConfiguration} annotation.
178+
* {@link ContextConfiguration @ContextConfiguration} annotation, either explicitly
179+
* or implicitly.
162180
* @return the declaring class (never {@code null})
163181
*/
164182
public Class<?> getDeclaringClass() {

spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2016 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.
@@ -29,7 +29,6 @@
2929
import org.springframework.test.context.MergedContextConfiguration;
3030
import org.springframework.test.context.SmartContextLoader;
3131
import org.springframework.util.Assert;
32-
import org.springframework.util.ObjectUtils;
3332

3433
/**
3534
* {@code AbstractDelegatingSmartContextLoader} serves as an abstract base class
@@ -202,15 +201,6 @@ else if (configAttributes.hasClasses()) {
202201
name(getAnnotationConfigLoader()), configAttributes));
203202
}
204203

205-
// If neither loader detected defaults and no initializers were declared,
206-
// throw an exception.
207-
if (!configAttributes.hasResources() && ObjectUtils.isEmpty(configAttributes.getInitializers())) {
208-
throw new IllegalStateException(String.format(
209-
"Neither %s nor %s was able to detect defaults, and no ApplicationContextInitializers "
210-
+ "were declared for context configuration %s", name(getXmlLoader()),
211-
name(getAnnotationConfigLoader()), configAttributes));
212-
}
213-
214204
if (configAttributes.hasLocations() && configAttributes.hasClasses()) {
215205
String message = String.format(
216206
"Configuration error: both default locations AND default configuration classes "

spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java

Lines changed: 74 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 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.
@@ -18,6 +18,7 @@
1818

1919
import java.util.ArrayList;
2020
import java.util.Arrays;
21+
import java.util.Collection;
2122
import java.util.Collections;
2223
import java.util.HashSet;
2324
import java.util.LinkedHashSet;
@@ -30,8 +31,6 @@
3031

3132
import org.springframework.beans.BeanInstantiationException;
3233
import org.springframework.beans.BeanUtils;
33-
import org.springframework.context.ApplicationContextInitializer;
34-
import org.springframework.context.ConfigurableApplicationContext;
3534
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
3635
import org.springframework.core.annotation.AnnotationUtils;
3736
import org.springframework.core.io.support.SpringFactoriesLoader;
@@ -71,6 +70,7 @@
7170
*
7271
* @author Sam Brannen
7372
* @author Juergen Hoeller
73+
* @author Phillip Webb
7474
* @since 4.1
7575
*/
7676
public abstract class AbstractTestContextBootstrapper implements TestContextBootstrapper {
@@ -272,13 +272,8 @@ public final MergedContextConfiguration buildMergedContextConfiguration() {
272272
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = getCacheAwareContextLoaderDelegate();
273273

274274
if (MetaAnnotationUtils.findAnnotationDescriptorForTypes(testClass, ContextConfiguration.class,
275-
ContextHierarchy.class) == null) {
276-
if (logger.isInfoEnabled()) {
277-
logger.info(String.format(
278-
"Neither @ContextConfiguration nor @ContextHierarchy found for test class [%s]",
279-
testClass.getName()));
280-
}
281-
return new MergedContextConfiguration(testClass, null, null, null, null);
275+
ContextHierarchy.class) == null) {
276+
return buildDefaultMergedContextConfiguration(testClass, cacheAwareContextLoaderDelegate);
282277
}
283278

284279
if (AnnotationUtils.findAnnotation(testClass, ContextHierarchy.class) != null) {
@@ -297,7 +292,7 @@ public final MergedContextConfiguration buildMergedContextConfiguration() {
297292
Class<?> declaringClass = reversedList.get(0).getDeclaringClass();
298293

299294
mergedConfig = buildMergedContextConfiguration(declaringClass, reversedList, parentConfig,
300-
cacheAwareContextLoaderDelegate);
295+
cacheAwareContextLoaderDelegate, true);
301296
parentConfig = mergedConfig;
302297
}
303298

@@ -307,10 +302,29 @@ public final MergedContextConfiguration buildMergedContextConfiguration() {
307302
else {
308303
return buildMergedContextConfiguration(testClass,
309304
ContextLoaderUtils.resolveContextConfigurationAttributes(testClass), null,
310-
cacheAwareContextLoaderDelegate);
305+
cacheAwareContextLoaderDelegate, true);
311306
}
312307
}
313308

309+
/**
310+
* @since 4.3
311+
*/
312+
private MergedContextConfiguration buildDefaultMergedContextConfiguration(Class<?> testClass,
313+
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) {
314+
315+
List<ContextConfigurationAttributes> defaultConfigAttributesList
316+
= Collections.singletonList(new ContextConfigurationAttributes(testClass));
317+
318+
ContextLoader contextLoader = resolveContextLoader(testClass, defaultConfigAttributesList);
319+
if (logger.isInfoEnabled()) {
320+
logger.info(String.format(
321+
"Neither @ContextConfiguration nor @ContextHierarchy found for test class [%s], using %s",
322+
testClass.getName(), contextLoader.getClass().getSimpleName()));
323+
}
324+
return buildMergedContextConfiguration(testClass, defaultConfigAttributesList, null,
325+
cacheAwareContextLoaderDelegate, false);
326+
}
327+
314328
/**
315329
* Build the {@link MergedContextConfiguration merged context configuration}
316330
* for the supplied {@link Class testClass}, context configuration attributes,
@@ -324,6 +338,9 @@ public final MergedContextConfiguration buildMergedContextConfiguration() {
324338
* context in a context hierarchy, or {@code null} if there is no parent
325339
* @param cacheAwareContextLoaderDelegate the cache-aware context loader delegate to
326340
* be passed to the {@code MergedContextConfiguration} constructor
341+
* @param requireLocationsClassesOrInitializers whether locations, classes, or
342+
* initializers are required; typically {@code true} but may be set to {@code false}
343+
* if the configured loader supports empty configuration
327344
* @return the merged context configuration
328345
* @see #resolveContextLoader
329346
* @see ContextLoaderUtils#resolveContextConfigurationAttributes
@@ -335,11 +352,15 @@ public final MergedContextConfiguration buildMergedContextConfiguration() {
335352
*/
336353
private MergedContextConfiguration buildMergedContextConfiguration(Class<?> testClass,
337354
List<ContextConfigurationAttributes> configAttributesList, MergedContextConfiguration parentConfig,
338-
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) {
355+
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate,
356+
boolean requireLocationsClassesOrInitializers) {
357+
358+
Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be null or empty");
339359

340360
ContextLoader contextLoader = resolveContextLoader(testClass, configAttributesList);
341-
List<String> locationsList = new ArrayList<String>();
342-
List<Class<?>> classesList = new ArrayList<Class<?>>();
361+
List<String> locations = new ArrayList<String>();
362+
List<Class<?>> classes = new ArrayList<Class<?>>();
363+
List<Class<?>> initializers = new ArrayList<Class<?>>();
343364

344365
for (ContextConfigurationAttributes configAttributes : configAttributesList) {
345366
if (logger.isTraceEnabled()) {
@@ -349,34 +370,53 @@ private MergedContextConfiguration buildMergedContextConfiguration(Class<?> test
349370
if (contextLoader instanceof SmartContextLoader) {
350371
SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader;
351372
smartContextLoader.processContextConfiguration(configAttributes);
352-
locationsList.addAll(0, Arrays.asList(configAttributes.getLocations()));
353-
classesList.addAll(0, Arrays.asList(configAttributes.getClasses()));
373+
locations.addAll(0, Arrays.asList(configAttributes.getLocations()));
374+
classes.addAll(0, Arrays.asList(configAttributes.getClasses()));
354375
}
355376
else {
356-
String[] processedLocations = contextLoader.processLocations(configAttributes.getDeclaringClass(),
357-
configAttributes.getLocations());
358-
locationsList.addAll(0, Arrays.asList(processedLocations));
377+
String[] processedLocations = contextLoader.processLocations(
378+
configAttributes.getDeclaringClass(), configAttributes.getLocations());
379+
locations.addAll(0, Arrays.asList(processedLocations));
359380
// Legacy ContextLoaders don't know how to process classes
360381
}
382+
initializers.addAll(0, Arrays.asList(configAttributes.getInitializers()));
361383
if (!configAttributes.isInheritLocations()) {
362384
break;
363385
}
364386
}
365387

366-
String[] locations = StringUtils.toStringArray(locationsList);
367-
Class<?>[] classes = ClassUtils.toClassArray(classesList);
368-
Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses = //
369-
ApplicationContextInitializerUtils.resolveInitializerClasses(configAttributesList);
370-
String[] activeProfiles = ActiveProfilesUtils.resolveActiveProfiles(testClass);
371-
MergedTestPropertySources mergedTestPropertySources = TestPropertySourceUtils.buildMergedTestPropertySources(testClass);
388+
if (requireLocationsClassesOrInitializers && areAllEmpty(locations, classes, initializers)) {
389+
throw new IllegalStateException(String.format(
390+
"%s was unable to detect defaults, and no ApplicationContextInitializers "
391+
+ "were declared for context configuration attributes %s",
392+
contextLoader.getClass().getSimpleName(), configAttributesList));
393+
}
372394

373-
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(testClass, locations, classes,
374-
initializerClasses, activeProfiles, mergedTestPropertySources.getLocations(),
375-
mergedTestPropertySources.getProperties(), contextLoader, cacheAwareContextLoaderDelegate, parentConfig);
395+
MergedTestPropertySources mergedTestPropertySources = TestPropertySourceUtils.buildMergedTestPropertySources(testClass);
396+
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(testClass,
397+
StringUtils.toStringArray(locations),
398+
ClassUtils.toClassArray(classes),
399+
ApplicationContextInitializerUtils.resolveInitializerClasses(configAttributesList),
400+
ActiveProfilesUtils.resolveActiveProfiles(testClass),
401+
mergedTestPropertySources.getLocations(),
402+
mergedTestPropertySources.getProperties(),
403+
contextLoader, cacheAwareContextLoaderDelegate, parentConfig);
376404

377405
return processMergedContextConfiguration(mergedConfig);
378406
}
379407

408+
/**
409+
* @since 4.3
410+
*/
411+
private boolean areAllEmpty(Collection<?>... collections) {
412+
for (Collection<?> collection : collections) {
413+
if (!collection.isEmpty()) {
414+
return false;
415+
}
416+
}
417+
return true;
418+
}
419+
380420
/**
381421
* Resolve the {@link ContextLoader} {@linkplain Class class} to use for the
382422
* supplied list of {@link ContextConfigurationAttributes} and then instantiate
@@ -389,7 +429,7 @@ private MergedContextConfiguration buildMergedContextConfiguration(Class<?> test
389429
* @param testClass the test class for which the {@code ContextLoader} should be
390430
* resolved; must not be {@code null}
391431
* @param configAttributesList the list of configuration attributes to process; must
392-
* not be {@code null} or <em>empty</em>; must be ordered <em>bottom-up</em>
432+
* not be {@code null}; must be ordered <em>bottom-up</em>
393433
* (i.e., as if we were traversing up the class hierarchy)
394434
* @return the resolved {@code ContextLoader} for the supplied {@code testClass}
395435
* (never {@code null})
@@ -400,7 +440,7 @@ protected ContextLoader resolveContextLoader(Class<?> testClass,
400440
List<ContextConfigurationAttributes> configAttributesList) {
401441

402442
Assert.notNull(testClass, "Class must not be null");
403-
Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty");
443+
Assert.notNull(configAttributesList, "ContextConfigurationAttributes list must not be null");
404444

405445
Class<? extends ContextLoader> contextLoaderClass = resolveExplicitContextLoaderClass(configAttributesList);
406446
if (contextLoaderClass == null) {
@@ -429,7 +469,7 @@ protected ContextLoader resolveContextLoader(Class<?> testClass,
429469
* step #1.</li>
430470
* </ol>
431471
* @param configAttributesList the list of configuration attributes to process;
432-
* must not be {@code null} or <em>empty</em>; must be ordered <em>bottom-up</em>
472+
* must not be {@code null}; must be ordered <em>bottom-up</em>
433473
* (i.e., as if we were traversing up the class hierarchy)
434474
* @return the {@code ContextLoader} class to use for the supplied configuration
435475
* attributes, or {@code null} if no explicit loader is found
@@ -439,7 +479,8 @@ protected ContextLoader resolveContextLoader(Class<?> testClass,
439479
protected Class<? extends ContextLoader> resolveExplicitContextLoaderClass(
440480
List<ContextConfigurationAttributes> configAttributesList) {
441481

442-
Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty");
482+
Assert.notNull(configAttributesList, "ContextConfigurationAttributes list must not be null");
483+
443484
for (ContextConfigurationAttributes configAttributes : configAttributesList) {
444485
if (logger.isTraceEnabled()) {
445486
logger.trace(String.format("Resolving ContextLoader for context configuration attributes %s",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2002-2016 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.junit4;
18+
19+
import org.junit.Test;
20+
import org.junit.runner.RunWith;
21+
22+
import org.springframework.beans.factory.annotation.Autowired;
23+
import org.springframework.context.annotation.Bean;
24+
import org.springframework.context.annotation.Configuration;
25+
26+
import static org.junit.Assert.assertEquals;
27+
28+
/**
29+
* JUnit 4 based integration test which verifies that {@link @ContextConfiguration}
30+
* is optional.
31+
*
32+
* @author Phillip Webb
33+
* @author Sam Brannen
34+
* @since 4.3
35+
*/
36+
@RunWith(SpringRunner.class)
37+
public class OptionalContextConfigurationSpringRunnerTests {
38+
39+
@Autowired
40+
String foo;
41+
42+
43+
@Test
44+
public void contextConfigurationAnnotationIsOptional() {
45+
assertEquals("foo", foo);
46+
}
47+
48+
49+
@Configuration
50+
static class Config {
51+
52+
@Bean
53+
String foo() {
54+
return "foo";
55+
}
56+
}
57+
58+
}

0 commit comments

Comments
 (0)