In this recipe, we will replace an existing bean with a test double using Spring's code configuration.
Let's assume that our system under test is the tax transferring system for a given person, as shown in the following code:
public class TaxTransferer { private final TaxService taxService; public TaxTransferer(TaxService taxService) { this.taxService = taxService; } public boolean transferTaxFor(Person person) { if (person == null) { return false; } taxService.transferTaxFor(person); return true; } }
Where
TaxService
is a class that makes the web service call, as shown in the following code (for simplicity, we are only writing that we are performing such data exchange):
class TaxService { public void transferTaxFor(Person person) { System.out.printf("Calling external web service for person with name [%s]%n", person.getName()); } }
Let's assume that we have an annotation-based configuration, as shown in the following code:
@Configuration class TaxConfiguration { @Bean public TaxService taxService() { return new TaxService(); } @Bean public TaxTransferer taxTransferer(TaxService taxService) { return new TaxTransferer(taxService); } }
In order to perform an integration test of the system and replace the bean with a mock, you have to perform the following steps:
@Configuration
annotated class.@Bean
methods that return a mock or a spy.The following snippet depicts the aforementioned scenario (the example is written for JUnit—for TestNG, consult the next information box following the snippet. Note that BDDAssertions
static imports are used—please refer to Chapter 7, Verifying Behavior with Object Matchers, for AssertJ configuration).
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {TaxConfiguration.class, MockTaxConfiguration.class}) public class TaxTransfererCodeConfigurationTest { @Autowired TaxTransferer taxTransferer; @Autowired TaxService taxService; @Test public void should_transfer_tax_for_person() { // given Person person = new Person(); // when boolean transferSuccessful = taxTransferer.transferTaxFor(person); // then then(transferSuccessful).isTrue(); verify(taxService).transferTaxFor(person); } }
The additional test configuration is as follows:
@Configuration class MockTaxConfiguration { @Bean public TaxService taxService() { return Mockito.mock(TaxService.class); } }
There might be cases where you do want your component to perform real logic. However, you want to confirm that a particular method was executed. Let's imagine a business case where you want to ensure that a particular web service method was called. In that case, you should return a spy instead of a mock by using return Mockito.spy(new TaxService());
.
How Spring internally works is a subject for several books, so we will not go deep into details but what is worth mentioning is that by providing the context configuration with the production and test configuration, we are overriding the initial bean definition as follows (note that method names have to match):
@ContextConfiguration(classes = {TaxConfiguration.class, MockTaxConfiguration.class})
In the logs, you will then see the following code:
INFO: Overriding bean definition for bean 'taxService': replacing [… cropped for redability purposes...; defined in class com.blogspot.toomuchcoding.book.chapter9.InjectingWithSpring.TaxConfiguration] with [… cropped for redability purposes...; defined in class com.blogspot.toomuchcoding.book.chapter9.InjectingWithSpring.MockTaxConfiguration]
In the integration test example, we had a single test and we didn't explicitly stub the mock's methods. If we had several tests, we most probably would like to stub the mock's behavior in a different manner in each test.
Remember that since such a created mock is a singleton bean (refer to http://docs.spring.io/spring/docs/4.0.5.RELEASE/spring-framework-reference/html/beans.html#beans-factory-scopes-singleton), then once stubbed it will be reused in all of your tests that use the same configuration.
To change that behavior, you would have to reset the mock by calling Mockito.reset(mock1, mock2…mockn)
and then stub the mock again.
You may observe different behavior when having a @Configuration
class that is annotated with @ComponentScan
. If you scan for components, then each @Component
annotated class will be treated as a singleton bean. Let's assume that our TaxService
is annotated as @Component
and that it's injected through the field and not the constructor. The following is the application context configuration:
@Configuration @ComponentScan("com.blogspot.toomuchcoding.book.chapter9.InjectingWithSpringComponentScan") class TaxConfiguration { }
Next, you can find the @Component
annotated TaxService
class definition:
@Component class TaxService { public void transferTaxFor(Person person) { System.out.printf("Calling external web service from @Component annotated class for person with name [%s]%n", person.getName()); } }
The Following is the TaxTransferer
class that is the point of entry of our integration test:
@Component public class TaxTransferer { @Autowired private TaxService taxService; public boolean transferTaxFor(Person person) { if (person == null) { return false; } taxService.transferTaxFor(person); return true; } }
Under the hood, Spring is instantiating beans by using BeanPostProcessors
. Even if you create your mock configuration like the one presented in the previous snippets, it will not work and you will get the following log message:
INFO: Skipping bean definition for [BeanMethod:name=taxService,declaringClass=com.blogspot.toomuchcoding.book.chapter9.InjectingWithSpringComponentScan.MockTaxConfiguration]: a definition for bean 'taxService' already exists. This top-level bean definition is considered as an override.
If possible, you should not annotate your classes with @Component
since you will limit the possibility of configuring your application. Imagine that components are building blocks and the @Configuration
annotated classes are blueprints of your application. In part of your applications, you will need some components that are not necessary in others. If you share the @Component
annotated beans in jars where you have component scanning, then most likely you will have in your Spring application context plenty of beans that you don't really need. You should only use classes that you really need. Please consult Spring's source code to verify that Spring itself doesn't use @Component
to instantiate its beans.
Let's assume that the @Component
annotated beans are already there and before refactoring you would like to test your application. There is a possibility of using Spring's internals to manage and mock the bean. Since the @Component
annotated class has been instantiated using BeanPostProcessors
, you can create your own class that will create a mock of the object we are interested in (the test will look exactly the same as in the previous test—the implementation of MockTaxConfiguration
will differ) as follows:
@Configuration class MockTaxConfiguration { @Bean public BeanPostProcessor taxServiceBeanPostProcessor() { return new BeanPostProcessor(){ @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if(bean instanceof TaxService) { return Mockito.mock(TaxService.class); } return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } }; } }
13.58.51.228