Skip to content

Commit e85e976

Browse files
committed
Fail if multiple @BootstrapWith's are present
Prior to this commit it was possible for two @BootstrapWith annotations to be 'present' on a test class -- for example, via competing custom composed annotations. However, only one of the annotations will ever be used to bootstrap the TestContext Framework. Thus, in such scenarios one of the annotations will be silently ignored. This commit introduces a check for such scenarios. BootstrapUtils' resolveTestContextBootstrapper() method now throws an IllegalStateException if more than one @BootstrapWith annotation is 'present' on a given test class. Issue: SPR-12602
1 parent 706d3ad commit e85e976

File tree

2 files changed

+150
-5
lines changed

2 files changed

+150
-5
lines changed

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@
1717
package org.springframework.test.context;
1818

1919
import java.lang.reflect.Constructor;
20+
import java.util.List;
2021

2122
import org.apache.commons.logging.Log;
2223
import org.apache.commons.logging.LogFactory;
2324

25+
import org.springframework.core.annotation.AnnotatedElementUtils;
26+
import org.springframework.core.annotation.AnnotationUtils;
2427
import org.springframework.util.ClassUtils;
28+
import org.springframework.util.MultiValueMap;
2529

2630
import static org.springframework.beans.BeanUtils.*;
27-
import static org.springframework.core.annotation.AnnotationUtils.*;
2831

2932
/**
3033
* {@code BootstrapUtils} is a collection of utility methods to assist with
@@ -120,17 +123,28 @@ static TestContextBootstrapper resolveTestContextBootstrapper(BootstrapContext b
120123

121124
Class<? extends TestContextBootstrapper> clazz = null;
122125
try {
123-
BootstrapWith bootstrapWith = findAnnotation(testClass, BootstrapWith.class);
124-
if (bootstrapWith != null && !TestContextBootstrapper.class.equals(bootstrapWith.value())) {
125-
clazz = bootstrapWith.value();
126+
127+
MultiValueMap<String, Object> attributesMultiMap = AnnotatedElementUtils.getAllAnnotationAttributes(
128+
testClass, BootstrapWith.class.getName());
129+
List<Object> values = (attributesMultiMap == null ? null : attributesMultiMap.get(AnnotationUtils.VALUE));
130+
131+
if (values != null) {
132+
if (values.size() != 1) {
133+
String msg = String.format(
134+
"Configuration error: found multiple declarations of @BootstrapWith on test class [%s] with values %s",
135+
testClass.getName(), values);
136+
throw new IllegalStateException(msg);
137+
}
138+
clazz = (Class<? extends TestContextBootstrapper>) values.get(0);
126139
}
127140
else {
128141
clazz = (Class<? extends TestContextBootstrapper>) ClassUtils.forName(
129142
DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, BootstrapUtils.class.getClassLoader());
130143
}
131144

132145
if (logger.isDebugEnabled()) {
133-
logger.debug(String.format("Instantiating TestContextBootstrapper from class [%s]", clazz.getName()));
146+
logger.debug(String.format("Instantiating TestContextBootstrapper for test class [%s] from class [%s]",
147+
testClass.getName(), clazz.getName()));
134148
}
135149

136150
TestContextBootstrapper testContextBootstrapper = instantiateClass(clazz, TestContextBootstrapper.class);
@@ -139,6 +153,10 @@ static TestContextBootstrapper resolveTestContextBootstrapper(BootstrapContext b
139153
return testContextBootstrapper;
140154
}
141155
catch (Throwable t) {
156+
if (t instanceof IllegalStateException) {
157+
throw (IllegalStateException) t;
158+
}
159+
142160
throw new IllegalStateException("Could not load TestContextBootstrapper [" + clazz
143161
+ "]. Specify @BootstrapWith's 'value' attribute "
144162
+ "or make the default bootstrapper class available.", t);
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2002-2015 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.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
22+
import org.junit.Rule;
23+
import org.junit.Test;
24+
import org.junit.rules.ExpectedException;
25+
26+
import org.springframework.test.context.support.DefaultTestContextBootstrapper;
27+
28+
import static org.hamcrest.CoreMatchers.*;
29+
import static org.junit.Assert.*;
30+
import static org.mockito.Mockito.*;
31+
import static org.springframework.test.context.BootstrapUtils.*;
32+
33+
/**
34+
* Unit tests for {@link BootstrapUtils}.
35+
*
36+
* @author Sam Brannen
37+
* @since 4.2
38+
*/
39+
public class BootstrapUtilsTests {
40+
41+
private final CacheAwareContextLoaderDelegate delegate = mock(CacheAwareContextLoaderDelegate.class);
42+
43+
@Rule
44+
public ExpectedException exception = ExpectedException.none();
45+
46+
@Test
47+
public void resolveTestContextBootstrapperForNonAnnotatedClass() {
48+
assertBootstrapper(NonAnnotatedClass.class, DefaultTestContextBootstrapper.class);
49+
}
50+
51+
@Test
52+
public void resolveTestContextBootstrapperWithEmptyBootstrapWithAnnotation() {
53+
BootstrapContext bootstrapContext = BootstrapTestUtils.buildBootstrapContext(EmptyBootstrapWithAnnotationClass.class, delegate);
54+
55+
exception.expect(IllegalStateException.class);
56+
exception.expectMessage(containsString("Specify @BootstrapWith's 'value' attribute"));
57+
58+
resolveTestContextBootstrapper(bootstrapContext);
59+
}
60+
61+
@Test
62+
public void resolveTestContextBootstrapperWithDirectBootstrapWithAnnotation() {
63+
assertBootstrapper(DirectBootstrapWithAnnotationClass.class, FooBootstrapper.class);
64+
}
65+
66+
@Test
67+
public void resolveTestContextBootstrapperWithInheritedBootstrapWithAnnotation() {
68+
assertBootstrapper(InheritedBootstrapWithAnnotationClass.class, FooBootstrapper.class);
69+
}
70+
71+
@Test
72+
public void resolveTestContextBootstrapperWithMetaBootstrapWithAnnotation() {
73+
assertBootstrapper(MetaAnnotatedBootstrapWithAnnotationClass.class, BarBootstrapper.class);
74+
}
75+
76+
@Test
77+
public void resolveTestContextBootstrapperWithDoubleMetaBootstrapWithAnnotation() {
78+
BootstrapContext bootstrapContext = BootstrapTestUtils.buildBootstrapContext(
79+
DoubleMetaAnnotatedBootstrapWithAnnotationClass.class, delegate);
80+
81+
exception.expect(IllegalStateException.class);
82+
exception.expectMessage(containsString("found multiple declarations of @BootstrapWith"));
83+
exception.expectMessage(containsString(FooBootstrapper.class.getName()));
84+
exception.expectMessage(containsString(BarBootstrapper.class.getName()));
85+
86+
resolveTestContextBootstrapper(bootstrapContext);
87+
}
88+
89+
private void assertBootstrapper(Class<?> testClass, Class<?> expectedBootstrapper) {
90+
BootstrapContext bootstrapContext = BootstrapTestUtils.buildBootstrapContext(testClass, delegate);
91+
TestContextBootstrapper bootstrapper = resolveTestContextBootstrapper(bootstrapContext);
92+
assertNotNull(bootstrapper);
93+
assertEquals(expectedBootstrapper, bootstrapper.getClass());
94+
}
95+
96+
// -------------------------------------------------------------------
97+
98+
static class FooBootstrapper extends DefaultTestContextBootstrapper {}
99+
100+
static class BarBootstrapper extends DefaultTestContextBootstrapper {}
101+
102+
@BootstrapWith(FooBootstrapper.class)
103+
@Retention(RetentionPolicy.RUNTIME)
104+
static @interface BootWithFoo {}
105+
106+
@BootstrapWith(BarBootstrapper.class)
107+
@Retention(RetentionPolicy.RUNTIME)
108+
static @interface BootWithBar {}
109+
110+
static class NonAnnotatedClass {}
111+
112+
@BootstrapWith
113+
static class EmptyBootstrapWithAnnotationClass {}
114+
115+
@BootstrapWith(FooBootstrapper.class)
116+
static class DirectBootstrapWithAnnotationClass {}
117+
118+
static class InheritedBootstrapWithAnnotationClass extends DirectBootstrapWithAnnotationClass {}
119+
120+
@BootWithBar
121+
static class MetaAnnotatedBootstrapWithAnnotationClass {}
122+
123+
@BootWithBar
124+
@BootWithFoo
125+
static class DoubleMetaAnnotatedBootstrapWithAnnotationClass {}
126+
127+
}

0 commit comments

Comments
 (0)