Injecting test doubles instead of beans using Spring's code configuration

In this recipe, we will replace an existing bean with a test double using Spring's code configuration.

Getting ready

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);
    }

}

How to do it...

In order to perform an integration test of the system and replace the bean with a mock, you have to perform the following steps:

  1. Write an integration test that sets the application context of the system under test.
  2. Create an additional @Configuration annotated class.
  3. Override the existing beans (method names have to match) that you want to mock with @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);
    }

}

Note

For TestNG, the only thing that changes is that you do not use the @RunWith(SpringJUnit4ClassRunner.class) annotation but instead you make the test class extend the AbstractTestNGSpringContextTests class.

The additional test configuration is as follows:

@Configuration
class MockTaxConfiguration {

    @Bean
    public TaxService taxService() {
        return Mockito.mock(TaxService.class);
    }

}

Note

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 it works...

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.

Note

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.

There's more...

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;
            }
        };
    }

}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.58.51.228