Skip to content

Commit aad4009

Browse files
committed
Make Mock/Spy qualifiers part of context cache key
Refine @MockBean/@SpyBean qualifier support so that qualifiers form part of the context cache key. Prior to this commit is was possible that two different tests could accidentally share the same context if they defined the same @mock but with different @qualifiers. See gh-6753
1 parent 04448d6 commit aad4009

File tree

11 files changed

+379
-91
lines changed

11 files changed

+379
-91
lines changed

spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/Definition.java

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
package org.springframework.boot.test.mock.mockito;
1818

19-
import java.lang.reflect.AnnotatedElement;
20-
2119
import org.springframework.util.ObjectUtils;
2220

2321
/**
@@ -30,28 +28,20 @@ abstract class Definition {
3028

3129
private static final int MULTIPLIER = 31;
3230

33-
private final AnnotatedElement element;
34-
3531
private final String name;
3632

3733
private final MockReset reset;
3834

3935
private final boolean proxyTargetAware;
4036

41-
Definition(AnnotatedElement element, String name, MockReset reset,
42-
boolean proxyTargetAware) {
43-
this.element = element;
37+
private final QualifierDefinition qualifier;
38+
39+
Definition(String name, MockReset reset, boolean proxyTargetAware,
40+
QualifierDefinition qualifier) {
4441
this.name = name;
4542
this.reset = (reset != null ? reset : MockReset.AFTER);
4643
this.proxyTargetAware = proxyTargetAware;
47-
}
48-
49-
/**
50-
* Return the {@link AnnotatedElement} that holds this definition.
51-
* @return the element that defines this definition or {@code null}
52-
*/
53-
public AnnotatedElement getElement() {
54-
return this.element;
44+
this.qualifier = qualifier;
5545
}
5646

5747
/**
@@ -78,13 +68,22 @@ public boolean isProxyTargetAware() {
7868
return this.proxyTargetAware;
7969
}
8070

71+
/**
72+
* Return the qualifier or {@code null}.
73+
* @return the qualifier
74+
*/
75+
public QualifierDefinition getQualifier() {
76+
return this.qualifier;
77+
}
78+
8179
@Override
8280
public int hashCode() {
8381
int result = 1;
8482
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.name);
8583
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.reset);
8684
result = MULTIPLIER * result
8785
+ ObjectUtils.nullSafeHashCode(this.proxyTargetAware);
86+
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.qualifier);
8887
return result;
8988
}
9089

@@ -102,6 +101,7 @@ public boolean equals(Object obj) {
102101
result &= ObjectUtils.nullSafeEquals(this.reset, other.reset);
103102
result &= ObjectUtils.nullSafeEquals(this.proxyTargetAware,
104103
other.proxyTargetAware);
104+
result &= ObjectUtils.nullSafeEquals(this.qualifier, other.qualifier);
105105
return result;
106106
}
107107

spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/DefinitionsParser.java

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,11 @@ private void parseMockBeanAnnotation(MockBean annotation, AnnotatedElement eleme
9191
"The name attribute can only be used when mocking a single class");
9292
}
9393
for (ResolvableType typeToMock : typesToMock) {
94-
MockDefinition definition = new MockDefinition(element, annotation.name(),
95-
typeToMock, annotation.extraInterfaces(), annotation.answer(),
96-
annotation.serializable(), annotation.reset());
97-
addDefinition(definition, "mock");
94+
MockDefinition definition = new MockDefinition(annotation.name(), typeToMock,
95+
annotation.extraInterfaces(), annotation.answer(),
96+
annotation.serializable(), annotation.reset(),
97+
QualifierDefinition.forElement(element));
98+
addDefinition(element, definition, "mock");
9899
}
99100
}
100101

@@ -107,16 +108,17 @@ private void parseSpyBeanAnnotation(SpyBean annotation, AnnotatedElement element
107108
"The name attribute can only be used when spying a single class");
108109
}
109110
for (ResolvableType typeToSpy : typesToSpy) {
110-
SpyDefinition definition = new SpyDefinition(element, annotation.name(),
111-
typeToSpy, annotation.reset(), annotation.proxyTargetAware());
112-
addDefinition(definition, "spy");
111+
SpyDefinition definition = new SpyDefinition(annotation.name(), typeToSpy,
112+
annotation.reset(), annotation.proxyTargetAware(),
113+
QualifierDefinition.forElement(element));
114+
addDefinition(element, definition, "spy");
113115
}
114116
}
115117

116-
private void addDefinition(Definition definition, String type) {
118+
private void addDefinition(AnnotatedElement element, Definition definition,
119+
String type) {
117120
boolean isNewDefinition = this.definitions.add(definition);
118121
Assert.state(isNewDefinition, "Duplicate " + type + " definition " + definition);
119-
AnnotatedElement element = definition.getElement();
120122
if (element instanceof Field) {
121123
Field field = (Field) element;
122124
this.definitionFields.put(definition, field);

spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockDefinition.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.boot.test.mock.mockito;
1818

19-
import java.lang.reflect.AnnotatedElement;
2019
import java.util.Arrays;
2120
import java.util.Collections;
2221
import java.util.LinkedHashSet;
@@ -50,10 +49,10 @@ class MockDefinition extends Definition {
5049

5150
private final boolean serializable;
5251

53-
MockDefinition(AnnotatedElement element, String name, ResolvableType typeToMock,
54-
Class<?>[] extraInterfaces, Answers answer, boolean serializable,
55-
MockReset reset) {
56-
super(element, name, reset, false);
52+
MockDefinition(String name, ResolvableType typeToMock, Class<?>[] extraInterfaces,
53+
Answers answer, boolean serializable, MockReset reset,
54+
QualifierDefinition qualifier) {
55+
super(name, reset, false, qualifier);
5756
Assert.notNull(typeToMock, "TypeToMock must not be null");
5857
this.typeToMock = typeToMock;
5958
this.extraInterfaces = asClassSet(extraInterfaces);

spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.boot.test.mock.mockito;
1818

1919
import java.beans.PropertyDescriptor;
20-
import java.lang.reflect.AnnotatedElement;
2120
import java.lang.reflect.Field;
2221
import java.util.Arrays;
2322
import java.util.HashMap;
@@ -44,7 +43,6 @@
4443
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
4544
import org.springframework.beans.factory.config.ConstructorArgumentValues;
4645
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
47-
import org.springframework.beans.factory.config.DependencyDescriptor;
4846
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter;
4947
import org.springframework.beans.factory.config.RuntimeBeanReference;
5048
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
@@ -209,9 +207,8 @@ private RootBeanDefinition createBeanDefinition(MockDefinition mockDefinition) {
209207
definition.setFactoryMethodName("createMock");
210208
definition.getConstructorArgumentValues().addIndexedArgumentValue(0,
211209
mockDefinition);
212-
AnnotatedElement element = mockDefinition.getElement();
213-
if (element instanceof Field) {
214-
definition.setQualifiedElement(element);
210+
if (mockDefinition.getQualifier() != null) {
211+
mockDefinition.getQualifier().applyTo(definition);
215212
}
216213
return definition;
217214
}
@@ -232,17 +229,17 @@ private String getBeanName(ConfigurableListableBeanFactory beanFactory,
232229
if (StringUtils.hasLength(mockDefinition.getName())) {
233230
return mockDefinition.getName();
234231
}
235-
String[] existingBeans = findCandidateBeans(beanFactory, mockDefinition);
236-
if (ObjectUtils.isEmpty(existingBeans)) {
232+
Set<String> existingBeans = findCandidateBeans(beanFactory, mockDefinition);
233+
if (existingBeans.isEmpty()) {
237234
return this.beanNameGenerator.generateBeanName(beanDefinition, registry);
238235
}
239-
if (existingBeans.length == 1) {
240-
return existingBeans[0];
236+
if (existingBeans.size() == 1) {
237+
return existingBeans.iterator().next();
241238
}
242239
throw new IllegalStateException(
243240
"Unable to register mock bean " + mockDefinition.getTypeToMock()
244241
+ " expected a single matching bean to replace but found "
245-
+ new TreeSet<String>(Arrays.asList(existingBeans)));
242+
+ existingBeans);
246243
}
247244

248245
private void registerSpy(ConfigurableListableBeanFactory beanFactory,
@@ -256,22 +253,17 @@ private void registerSpy(ConfigurableListableBeanFactory beanFactory,
256253
}
257254
}
258255

259-
private String[] findCandidateBeans(ConfigurableListableBeanFactory beanFactory,
256+
private Set<String> findCandidateBeans(ConfigurableListableBeanFactory beanFactory,
260257
MockDefinition mockDefinition) {
261-
String[] beans = getExistingBeans(beanFactory, mockDefinition.getTypeToMock());
262-
// Attempt to filter using qualifiers
263-
if (beans.length > 1 && mockDefinition.getElement() instanceof Field) {
264-
DependencyDescriptor descriptor = new DependencyDescriptor(
265-
(Field) mockDefinition.getElement(), true);
266-
Set<String> candidates = new LinkedHashSet<String>();
267-
for (String bean : beans) {
268-
if (beanFactory.isAutowireCandidate(bean, descriptor)) {
269-
candidates.add(bean);
270-
}
258+
QualifierDefinition qualifier = mockDefinition.getQualifier();
259+
Set<String> candidates = new TreeSet<String>();
260+
for (String candidate : getExistingBeans(beanFactory,
261+
mockDefinition.getTypeToMock())) {
262+
if (qualifier == null || qualifier.matches(beanFactory, candidate)) {
263+
candidates.add(candidate);
271264
}
272-
return candidates.toArray(new String[candidates.size()]);
273265
}
274-
return beans;
266+
return candidates;
275267
}
276268

277269
private String[] getExistingBeans(ConfigurableListableBeanFactory beanFactory,
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2012-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.boot.test.mock.mockito;
18+
19+
import java.lang.annotation.Annotation;
20+
import java.lang.reflect.AnnotatedElement;
21+
import java.lang.reflect.Field;
22+
import java.util.HashSet;
23+
import java.util.Set;
24+
25+
import org.springframework.beans.factory.annotation.Qualifier;
26+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
27+
import org.springframework.beans.factory.config.DependencyDescriptor;
28+
import org.springframework.beans.factory.support.RootBeanDefinition;
29+
import org.springframework.core.annotation.AnnotationUtils;
30+
31+
/**
32+
* Definition of a Spring {@link Qualifier @Qualifier}.
33+
*
34+
* @author Phillip Webb
35+
* @author Stephane Nicoll
36+
* @see Definition
37+
*/
38+
class QualifierDefinition {
39+
40+
private final Field field;
41+
42+
private final DependencyDescriptor descriptor;
43+
44+
private final Set<Annotation> annotations;
45+
46+
QualifierDefinition(Field field, Set<Annotation> annotations) {
47+
// We can't use the field or descriptor as part of the context key
48+
// but we can assume that if two fields have the same qualifiers then
49+
// it's safe for Spring to use either for qualifier logic
50+
this.field = field;
51+
this.descriptor = new DependencyDescriptor(field, true);
52+
this.annotations = annotations;
53+
}
54+
55+
public boolean matches(ConfigurableListableBeanFactory beanFactory, String beanName) {
56+
return beanFactory.isAutowireCandidate(beanName, this.descriptor);
57+
}
58+
59+
public void applyTo(RootBeanDefinition definition) {
60+
definition.setQualifiedElement(this.field);
61+
}
62+
63+
@Override
64+
public int hashCode() {
65+
return this.annotations.hashCode();
66+
}
67+
68+
@Override
69+
public boolean equals(Object obj) {
70+
if (obj == this) {
71+
return true;
72+
}
73+
if (obj == null || !getClass().isAssignableFrom(obj.getClass())) {
74+
return false;
75+
}
76+
QualifierDefinition other = (QualifierDefinition) obj;
77+
return this.annotations.equals(other.annotations);
78+
}
79+
80+
public static QualifierDefinition forElement(AnnotatedElement element) {
81+
if (element != null && element instanceof Field) {
82+
Field field = (Field) element;
83+
Set<Annotation> annotations = getQualifierAnnotations(field);
84+
if (!annotations.isEmpty()) {
85+
return new QualifierDefinition(field, annotations);
86+
}
87+
}
88+
return null;
89+
}
90+
91+
private static Set<Annotation> getQualifierAnnotations(Field field) {
92+
// Assume that any annotations other than @MockBean/@SpyBean are qualifiers
93+
Annotation[] candidates = field.getDeclaredAnnotations();
94+
Set<Annotation> annotations = new HashSet<Annotation>(candidates.length);
95+
for (Annotation candidate : candidates) {
96+
if (!isMockOrSpyAnnotation(candidate)) {
97+
annotations.add(candidate);
98+
}
99+
}
100+
return annotations;
101+
}
102+
103+
private static boolean isMockOrSpyAnnotation(Annotation candidate) {
104+
Class<? extends Annotation> type = candidate.annotationType();
105+
return (type.equals(MockBean.class) || type.equals(SpyBean.class)
106+
|| AnnotationUtils.isAnnotationMetaPresent(type, MockBean.class)
107+
|| AnnotationUtils.isAnnotationMetaPresent(type, SpyBean.class));
108+
}
109+
110+
}

spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/SpyDefinition.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
package org.springframework.boot.test.mock.mockito;
1818

19-
import java.lang.reflect.AnnotatedElement;
20-
2119
import org.mockito.MockSettings;
2220
import org.mockito.Mockito;
2321
import org.mockito.internal.util.MockUtil;
@@ -41,9 +39,9 @@ class SpyDefinition extends Definition {
4139

4240
private final ResolvableType typeToSpy;
4341

44-
SpyDefinition(AnnotatedElement element, String name, ResolvableType typeToSpy,
45-
MockReset reset, boolean proxyTargetAware) {
46-
super(element, name, reset, proxyTargetAware);
42+
SpyDefinition(String name, ResolvableType typeToSpy, MockReset reset,
43+
boolean proxyTargetAware, QualifierDefinition qualifier) {
44+
super(name, reset, proxyTargetAware, qualifier);
4745
Assert.notNull(typeToSpy, "TypeToSpy must not be null");
4846
this.typeToSpy = typeToSpy;
4947

0 commit comments

Comments
 (0)