Support Qualifiers on MockBean and SpyBean

Previously, if an injection point used a qualifier, `MockBean` and
`SpyBean` couldn't be used to mock/spy it as there was no way to
specify that qualifier information.

This commit now detects qualifier information on the injection point
and associate it with the created `BeanDefintion`. If one wants to
mock a bean that is qualified with `@Qualifier("foo")`, the definition
of the mock should be as follows:

```
public class MyTest {

	@MockBean
	@Qualifier("foo")
	private ExampleService service;
}
```

As a side effect, it is now possible to mock a service by type even if
there are multiple instances of that type in the application context. The
provided qualifier information is used to determine the right candidate
and the proper bean definition is replaced accordingly.

Closes gh-6753
This commit is contained in:
Stephane Nicoll
2016-09-16 11:17:23 +02:00
parent 3e19f8aa8d
commit 3f236dc951
15 changed files with 314 additions and 45 deletions

View File

@@ -28,6 +28,7 @@ import org.springframework.boot.test.mock.mockito.example.ExampleExtraInterface;
import org.springframework.boot.test.mock.mockito.example.ExampleService;
import org.springframework.boot.test.mock.mockito.example.ExampleServiceCaller;
import org.springframework.boot.test.mock.mockito.example.RealExampleService;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -66,6 +67,7 @@ public class DefinitionsParserTests {
this.parser.parse(MockBeanAttributes.class);
assertThat(getDefinitions()).hasSize(1);
MockDefinition definition = getMockDefinition(0);
assertThat(definition.getElement()).isEqualTo(MockBeanAttributes.class);
assertThat(definition.getName()).isEqualTo("Name");
assertThat(definition.getTypeToMock().resolve()).isEqualTo(ExampleService.class);
assertThat(definition.getExtraInterfaces())
@@ -79,9 +81,15 @@ public class DefinitionsParserTests {
public void parseMockBeanOnClassAndField() throws Exception {
this.parser.parse(MockBeanOnClassAndField.class);
assertThat(getDefinitions()).hasSize(2);
assertThat(getMockDefinition(0).getTypeToMock().resolve())
MockDefinition classDefinition = getMockDefinition(0);
assertThat(classDefinition.getElement())
.isEqualTo(MockBeanOnClassAndField.class);
assertThat(classDefinition.getTypeToMock().resolve())
.isEqualTo(ExampleService.class);
assertThat(getMockDefinition(1).getTypeToMock().resolve())
MockDefinition fieldDefinition = getMockDefinition(1);
assertThat(fieldDefinition.getElement()).isEqualTo(
ReflectionUtils.findField(MockBeanOnClassAndField.class, "caller"));
assertThat(fieldDefinition.getTypeToMock().resolve())
.isEqualTo(ExampleServiceCaller.class);
}
@@ -141,6 +149,7 @@ public class DefinitionsParserTests {
this.parser.parse(SpyBeanAttributes.class);
assertThat(getDefinitions()).hasSize(1);
SpyDefinition definition = getSpyDefinition(0);
assertThat(definition.getElement()).isEqualTo(SpyBeanAttributes.class);
assertThat(definition.getName()).isEqualTo("Name");
assertThat(definition.getTypeToSpy().resolve())
.isEqualTo(RealExampleService.class);
@@ -151,10 +160,14 @@ public class DefinitionsParserTests {
public void parseSpyBeanOnClassAndField() throws Exception {
this.parser.parse(SpyBeanOnClassAndField.class);
assertThat(getDefinitions()).hasSize(2);
assertThat(getSpyDefinition(0).getTypeToSpy().resolve())
SpyDefinition classDefinition = getSpyDefinition(0);
assertThat(classDefinition.getElement())
.isEqualTo(SpyBeanOnClassAndField.class);
assertThat(classDefinition.getTypeToSpy().resolve())
.isEqualTo(RealExampleService.class);
assertThat(getSpyDefinition(1).getTypeToSpy().resolve())
.isEqualTo(ExampleServiceCaller.class);
SpyDefinition fieldDefinition = getSpyDefinition(1);
assertThat(fieldDefinition.getElement()).isEqualTo(
ReflectionUtils.findField(SpyBeanOnClassAndField.class, "caller"));
}
@Test

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.test.mock.mockito;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.example.CustomQualifier;
import org.springframework.boot.test.mock.mockito.example.CustomQualifierExampleService;
import org.springframework.boot.test.mock.mockito.example.ExampleService;
import org.springframework.boot.test.mock.mockito.example.ExampleServiceCaller;
import org.springframework.boot.test.mock.mockito.example.RealExampleService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
/**
* Test {@link MockBean} on a test class field can be used to replace existing bean
* while preserving qualifiers.
*/
@RunWith(SpringRunner.class)
public class MockBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests {
@MockBean
@CustomQualifier
private ExampleService service;
@Autowired
private ExampleServiceCaller caller;
@Autowired
private ApplicationContext applicationContext;
@Test
public void testMocking() throws Exception {
this.caller.sayGreeting();
verify(this.service).greeting();
}
@Test
public void onlyQualifiedBeanIsReplaced() {
assertThat(this.applicationContext.getBean("service")).isSameAs(this.service);
ExampleService anotherService = this.applicationContext.getBean(
"anotherService", ExampleService.class);
assertThat(anotherService.greeting()).isEqualTo("Another");
}
@Configuration
static class TestConfig {
@Bean
public CustomQualifierExampleService service() {
return new CustomQualifierExampleService();
}
@Bean
public ExampleService anotherService() {
return new RealExampleService("Another");
}
@Bean
public ExampleServiceCaller controller(@CustomQualifier ExampleService service) {
return new ExampleServiceCaller(service);
}
}
}

View File

@@ -46,13 +46,14 @@ public class MockDefinitionTests {
public void classToMockMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("TypeToMock must not be null");
new MockDefinition(null, null, null, null, false, null);
new MockDefinition(null, null, null, null, null, false, null);
}
@Test
public void createWithDefaults() throws Exception {
MockDefinition definition = new MockDefinition(null, EXAMPLE_SERVICE_TYPE, null,
null, false, null);
MockDefinition definition = new MockDefinition(null, null, EXAMPLE_SERVICE_TYPE,
null, null, false, null);
assertThat(definition.getElement()).isNull();
assertThat(definition.getName()).isNull();
assertThat(definition.getTypeToMock()).isEqualTo(EXAMPLE_SERVICE_TYPE);
assertThat(definition.getExtraInterfaces()).isEmpty();
@@ -63,9 +64,11 @@ public class MockDefinitionTests {
@Test
public void createExplicit() throws Exception {
MockDefinition definition = new MockDefinition("name", EXAMPLE_SERVICE_TYPE,
MockDefinition definition = new MockDefinition(getClass(), "name",
EXAMPLE_SERVICE_TYPE,
new Class<?>[] { ExampleExtraInterface.class },
Answers.RETURNS_SMART_NULLS, true, MockReset.BEFORE);
assertThat(definition.getElement()).isEqualTo(getClass());
assertThat(definition.getName()).isEqualTo("name");
assertThat(definition.getTypeToMock()).isEqualTo(EXAMPLE_SERVICE_TYPE);
assertThat(definition.getExtraInterfaces())
@@ -78,7 +81,8 @@ public class MockDefinitionTests {
@Test
public void createMock() throws Exception {
MockDefinition definition = new MockDefinition("name", EXAMPLE_SERVICE_TYPE,
MockDefinition definition = new MockDefinition(null, "name",
EXAMPLE_SERVICE_TYPE,
new Class<?>[] { ExampleExtraInterface.class },
Answers.RETURNS_SMART_NULLS, true, MockReset.BEFORE);
ExampleService mock = definition.createMock();

View File

@@ -25,6 +25,7 @@ import org.junit.Test;
import org.springframework.boot.test.mock.mockito.example.ExampleService;
import org.springframework.boot.test.mock.mockito.example.ExampleServiceCaller;
import org.springframework.core.ResolvableType;
import static org.assertj.core.api.Assertions.assertThat;
@@ -39,8 +40,8 @@ public class MockitoContextCustomizerTests {
@Test
public void hashCodeAndEquals() {
MockDefinition d1 = new MockDefinition(ExampleService.class);
MockDefinition d2 = new MockDefinition(ExampleServiceCaller.class);
MockDefinition d1 = createTestMockDefinition(ExampleService.class);
MockDefinition d2 = createTestMockDefinition(ExampleServiceCaller.class);
MockitoContextCustomizer c1 = new MockitoContextCustomizer(NO_DEFINITIONS);
MockitoContextCustomizer c2 = new MockitoContextCustomizer(
new LinkedHashSet<MockDefinition>(Arrays.asList(d1, d2)));
@@ -51,4 +52,8 @@ public class MockitoContextCustomizerTests {
assertThat(c2).isEqualTo(c2).isEqualTo(c3).isNotEqualTo(c1);
}
private MockDefinition createTestMockDefinition(Class<?> typeToMock) {
return new MockDefinition(null, null, ResolvableType.forClass(typeToMock), null, null, false, null);
}
}

View File

@@ -22,6 +22,7 @@ import org.junit.rules.ExpectedException;
import org.mockito.internal.util.MockUtil;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.test.mock.mockito.example.ExampleService;
import org.springframework.boot.test.mock.mockito.example.FailingExampleService;
@@ -50,11 +51,24 @@ public class MockitoPostProcessorTests {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage(
"Unable to register mock bean " + ExampleService.class.getName()
+ " expected a single existing bean to replace "
+ " expected a single matching bean to replace "
+ "but found [example1, example2]");
context.refresh();
}
@Test
public void cannotMockMultipleQualifiedBeans() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
MockitoPostProcessor.register(context);
context.register(MultipleQualifiedBeans.class);
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage(
"Unable to register mock bean " + ExampleService.class.getName()
+ " expected a single matching bean to replace "
+ "but found [example1, example3]");
context.refresh();
}
@Test
public void canMockBeanProducedByFactoryBeanWithObjectTypeAttribute() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
@@ -96,6 +110,32 @@ public class MockitoPostProcessorTests {
}
@Configuration
static class MultipleQualifiedBeans {
@MockBean(ExampleService.class)
@Qualifier("test")
private ExampleService mock;
@Bean
@Qualifier("test")
public ExampleService example1() {
return new FailingExampleService();
}
@Bean
public ExampleService example2() {
return new FailingExampleService();
}
@Bean
@Qualifier("test")
public ExampleService example3() {
return new FailingExampleService();
}
}
static class TestFactoryBean implements FactoryBean<Object> {
@Override

View File

@@ -47,12 +47,13 @@ public class SpyDefinitionTests {
public void classToSpyMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("TypeToSpy must not be null");
new SpyDefinition(null, null, null, true);
new SpyDefinition(null, null, null, null, true);
}
@Test
public void createWithDefaults() throws Exception {
SpyDefinition definition = new SpyDefinition(null, REAL_SERVICE_TYPE, null, true);
SpyDefinition definition = new SpyDefinition(null, null, REAL_SERVICE_TYPE, null, true);
assertThat(definition.getElement()).isNull();
assertThat(definition.getName()).isNull();
assertThat(definition.getTypeToSpy()).isEqualTo(REAL_SERVICE_TYPE);
assertThat(definition.getReset()).isEqualTo(MockReset.AFTER);
@@ -61,8 +62,9 @@ public class SpyDefinitionTests {
@Test
public void createExplicit() throws Exception {
SpyDefinition definition = new SpyDefinition("name", REAL_SERVICE_TYPE,
MockReset.BEFORE, false);
SpyDefinition definition = new SpyDefinition(getClass(), "name",
REAL_SERVICE_TYPE, MockReset.BEFORE, false);
assertThat(definition.getElement()).isEqualTo(getClass());
assertThat(definition.getName()).isEqualTo("name");
assertThat(definition.getTypeToSpy()).isEqualTo(REAL_SERVICE_TYPE);
assertThat(definition.getReset()).isEqualTo(MockReset.BEFORE);
@@ -71,7 +73,7 @@ public class SpyDefinitionTests {
@Test
public void createSpy() throws Exception {
SpyDefinition definition = new SpyDefinition("name", REAL_SERVICE_TYPE,
SpyDefinition definition = new SpyDefinition(null, "name", REAL_SERVICE_TYPE,
MockReset.BEFORE, true);
RealExampleService spy = definition.createSpy(new RealExampleService("hello"));
MockCreationSettings<?> settings = new MockUtil().getMockSettings(spy);
@@ -84,7 +86,7 @@ public class SpyDefinitionTests {
@Test
public void createSpyWhenNullInstanceShouldThrowException() throws Exception {
SpyDefinition definition = new SpyDefinition("name", REAL_SERVICE_TYPE,
SpyDefinition definition = new SpyDefinition(null, "name", REAL_SERVICE_TYPE,
MockReset.BEFORE, true);
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Instance must not be null");
@@ -93,7 +95,7 @@ public class SpyDefinitionTests {
@Test
public void createSpyWhenWrongInstanceShouldThrowException() throws Exception {
SpyDefinition definition = new SpyDefinition("name", REAL_SERVICE_TYPE,
SpyDefinition definition = new SpyDefinition(null, "name", REAL_SERVICE_TYPE,
MockReset.BEFORE, true);
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("must be an instance of");
@@ -102,7 +104,7 @@ public class SpyDefinitionTests {
@Test
public void createSpyTwice() throws Exception {
SpyDefinition definition = new SpyDefinition("name", REAL_SERVICE_TYPE,
SpyDefinition definition = new SpyDefinition(null, "name", REAL_SERVICE_TYPE,
MockReset.BEFORE, true);
Object instance = new RealExampleService("hello");
instance = definition.createSpy(instance);

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.test.mock.mockito.example;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.springframework.beans.factory.annotation.Qualifier;
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomQualifier {
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.test.mock.mockito.example;
/**
* An {@link ExampleService} that uses a custom qualifier.
*
* @author Andy Wilkinson
*/
@CustomQualifier
public class CustomQualifierExampleService implements ExampleService {
@Override
public String greeting() {
return "CustomQualifier";
}
}