Hello World

[펌]How to mock Spring bean (version 2) 본문

Spring/Boot(4.x)

[펌]How to mock Spring bean (version 2)

EnterKey 2016. 1. 10. 11:56
반응형

About a year ago, I wrote a blog post how to mock Spring Bean. Patterns described there were little bit invasive to the production code.  As one of the readers Colin correctly pointed out in comment, there is better alternative to spy/mock Spring bean based on @Profile annotation. This blog post is going to describe this technique. I used this approach with success at work and also in my side projects.

Note that widespread mocking in your application is often considered as design smell.

Introducing production code

First of all we need code under test to demonstrate mocking. We will use these simple classes:

01@Repository
02public class AddressDao {
03    public String readAddress(String userName) {
04        return "3 Dark Corner";
05    }
06}
07 
08@Service
09public class AddressService {
10    private AddressDao addressDao;
11     
12    @Autowired
13    public AddressService(AddressDao addressDao) {
14        this.addressDao = addressDao;
15    }
16     
17    public String getAddressForUser(String userName){
18        return addressDao.readAddress(userName);
19    }
20}
21 
22@Service
23public class UserService {
24    private AddressService addressService;
25 
26    @Autowired
27    public UserService(AddressService addressService) {
28        this.addressService = addressService;
29    }
30     
31    public String getUserDetails(String userName){
32        String address = addressService.getAddressForUser(userName);
33        return String.format("User %s, %s", userName, address);
34    }
35}

Of course this code doesn’t make much sense, but will be good to demonstrate how to mock Spring bean. AddressDao just returns string and thus simulates read from some data source. It is autowired into AddressService. This bean is autowired into UserService, which is used to construct string with user name and address.

Notice that we are using constructor injection as field injection is considered as bad practice. If you want to enforce constructor injection for your application, Oliver Gierke (Spring ecosystem developer and Spring Data lead) recently created very nice project Ninjector.

Configuration which scans all these beans is pretty standard Spring Boot main class:

1@SpringBootApplication
2public class SimpleApplication {
3    public static void main(String[] args) {
4        SpringApplication.run(SimpleApplication.class, args);
5    }
6}

Mock Spring bean (without AOP)

Let’s test the AddressService class where we mock AddressDao. We can create this mock via Spring’ @Profiles and@Primary annotations this way:

1@Profile("AddressService-test")
2@Configuration
3public class AddressDaoTestConfiguration {
4    @Bean
5    @Primary
6    public AddressDao addressDao() {
7        return Mockito.mock(AddressDao.class);
8    }
9}

This test configuration will be applied only when Spring profile AddressService-test is active. When it’s applied, it registers bean of type AddressDao, which is mock instance created by Mockito@Primary annotation tells Spring to use this instance instead of real one when somebody autowire AddressDao bean.

Test class is using JUnit framework:

01@ActiveProfiles("AddressService-test")
02@RunWith(SpringJUnit4ClassRunner.class)
03@SpringApplicationConfiguration(SimpleApplication.class)
04public class AddressServiceITest {
05    @Autowired
06    private AddressService addressService;
07 
08    @Autowired
09    private AddressDao addressDao;
10 
11    @Test
12    public void testGetAddressForUser() {
13        // GIVEN
14        Mockito.when(addressDao.readAddress("john"))
15            .thenReturn("5 Bright Corner");
16 
17        // WHEN
18        String actualAddress = addressService.getAddressForUser("john");
19   
20        // THEN  
21        Assert.assertEquals("5 Bright Corner", actualAddress);
22    }
23}

We activate profile AddressService-test to enable AddressDao mocking. Annotation @RunWith is needed for Spring integration tests and @SpringApplicationConfiguration defines which Spring configuration will be used to construct context for testing. Before the test, we autowire instance of AddressService under test and AddressDao mock.

Subsequent testing method should be clear if you are using Mockito. In GIVEN phase, we record desired behavior into mock instance. In WHEN phase, we execute testing code and in THEN phase, we verify if testing code returned value we expect.

Spy on Spring Bean (without AOP)

For spying example, will be spying on AddressService instance:

1@Profile("UserService-test")
2@Configuration
3public class AddressServiceTestConfiguration {
4    @Bean
5    @Primary
6    public AddressService addressServiceSpy(AddressService addressService) {
7        return Mockito.spy(addressService);
8    }
9}

This Spring configuration will be component scanned only if profile UserService-test will be active. It defines primary bean of type AddressService@Primary tells Spring to use this instance in case two beans of this type are present in Spring context.  During construction of this bean we autowire existing instance of AddressService from Spring context and use Mockito’s spying feature. The bean we are registering is effectively delegating all the calls to original instance, but Mockito spying allows us to verify interactions on spied instance.

We will test behavior of UserService this way:

01@ActiveProfiles("UserService-test")
02@RunWith(SpringJUnit4ClassRunner.class)
03@SpringApplicationConfiguration(SimpleApplication.class)
04public class UserServiceITest {
05    @Autowired
06    private UserService userService;
07 
08    @Autowired
09    private AddressService addressService;
10  
11    @Test
12    public void testGetUserDetails() {
13        // GIVEN - Spring scanned by SimpleApplication class
14 
15        // WHEN
16        String actualUserDetails = userService.getUserDetails("john");
17  
18        // THEN
19        Assert.assertEquals("User john, 3 Dark Corner", actualUserDetails);
20        Mockito.verify(addressService).getAddressForUser("john");
21    }
22}

For testing we activate UserService-test profile so our spying configuration will be applied. We autowire UserService which is under test and AddressService, which is being spied via Mockito.

We don’t need to prepare any behavior for testing in GIVEN phase. WHEN phase is obviously executing code under test. InTHEN phase we verify if testing code returned value we expect and also if addressService call was executed with correct parameter.

Problems with Mockito and Spring AOP

Let say now we want to use Spring AOP module to handle some cross-cutting concerns. For example to log calls on our Spring beans this way:

01package net.lkrnac.blog.testing.mockbeanv2.aoptesting;
02 
03import org.aspectj.lang.JoinPoint;
04import org.aspectj.lang.annotation.Aspect;
05import org.aspectj.lang.annotation.Before;
06import org.springframework.context.annotation.Profile;
07import org.springframework.stereotype.Component;
08 
09import lombok.extern.slf4j.Slf4j;
10     
11@Aspect
12@Component
13@Slf4j
14@Profile("aop"//only for example purposes
15public class AddressLogger {
16    @Before("execution(* net.lkrnac.blog.testing.mockbeanv2.beans.*.*(..))")
17    public void logAddressCall(JoinPoint jp){
18        log.info("Executing method {}", jp.getSignature());
19    }
20}

This AOP Aspect is applied before call on Spring beans from package net.lkrnac.blog.testing.mockbeanv2. It is using Lombok’s annotation @Slf4j to log signature of called method. Notice that this bean is created only when aop profile is defined. We are using this profile to separate AOP and non-AOP testing examples. In a real application you wouldn’t want to use such profile.

We also need to enable AspectJ for our application, therefore all the following examples will be using this Spring Boot main class:

1@SpringBootApplication
2@EnableAspectJAutoProxy
3public class AopApplication {
4    public static void main(String[] args) {
5        SpringApplication.run(AopApplication.class, args);
6    }
7}

AOP constructs are enabled by @EnableAspectJAutoProxy.

But such AOP constructs may be problematic if we combine Mockito for mocking with Spring AOP. It is because both use CGLIB to proxy real instances and when Mockito proxy is wrapped into Spring proxy, we can experience type mismatch problems. These can be mitigated by configuring bean’s scope with ScopedProxyMode.TARGET_CLASS, but Mockito verify()calls still fail with NotAMockException. Such problems can be seen if we enable aop profile for UserServiceITest.

Mock Spring bean proxied by Spring AOP

To overcome these problems, we will wrap mock into this Spring bean:

01package net.lkrnac.blog.testing.mockbeanv2.aoptesting;
02 
03import org.mockito.Mockito;
04import org.springframework.context.annotation.Primary;
05import org.springframework.context.annotation.Profile;
06import org.springframework.stereotype.Repository;
07 
08import lombok.Getter;
09import net.lkrnac.blog.testing.mockbeanv2.beans.AddressDao;
10 
11@Primary
12@Repository
13@Profile("AddressService-aop-mock-test")
14public class AddressDaoMock extends AddressDao{
15    @Getter
16    private AddressDao mockDelegate = Mockito.mock(AddressDao.class);
17     
18    public String readAddress(String userName) {
19        return mockDelegate.readAddress(userName);
20    }
21}

@Primary annotation makes sure that this bean will take precedence before real AddressDao bean during injection. To make sure it will be applied only for specific test, we define profile AddressService-aop-mock-test for this bean. It inheritsAddressDao class, so that it can act as full replacement of that type.

In order to fake behavior, we define mock instance of type AddressDao, which is exposed via getter defined by Lombok’s@Getter annotation. We also implement readAddress() method which is expected to be called during test. This method just delegates the call to mock instance.

The test where this mock is used can look like this:

01@ActiveProfiles({"AddressService-aop-mock-test""aop"})
02@RunWith(SpringJUnit4ClassRunner.class)
03@SpringApplicationConfiguration(AopApplication.class)
04public class AddressServiceAopMockITest {
05    @Autowired
06    private AddressService addressService;
07 
08    @Autowired
09    private AddressDao addressDao;
10     
11    @Test
12    public void testGetAddressForUser() {
13        // GIVEN
14        AddressDaoMock addressDaoMock = (AddressDaoMock) addressDao;
15        Mockito.when(addressDaoMock.getMockDelegate().readAddress("john"))
16            .thenReturn("5 Bright Corner");
17  
18        // WHEN
19        String actualAddress = addressService.getAddressForUser("john");
20  
21        // THEN  
22        Assert.assertEquals("5 Bright Corner", actualAddress);
23    }
24}

In the test we define AddressService-aop-mock-test profile to activate AddressDaoMock and aop profile to activateAddressLogger AOP aspect. For testing, we autowire testing bean addressService and its faked dependency addressDao. As we know, addressDao will be of type AddressDaoMock, because this bean was marked as @Primary. Therefore we can cast it and record behavior into mockDelegate.

When we call testing method, recorded behavior should be used because we expect testing method to use AddressDaodependency.

Spy on Spring bean proxied by Spring AOP

Similar pattern can be used for spying the real implementation. This is how our spy can look like:

01package net.lkrnac.blog.testing.mockbeanv2.aoptesting;
02 
03import org.mockito.Mockito;
04import org.springframework.beans.factory.annotation.Autowired;
05import org.springframework.context.annotation.Primary;
06import org.springframework.context.annotation.Profile;
07import org.springframework.stereotype.Service;
08 
09import lombok.Getter;
10import net.lkrnac.blog.testing.mockbeanv2.beans.AddressDao;
11import net.lkrnac.blog.testing.mockbeanv2.beans.AddressService;
12 
13@Primary
14@Service
15@Profile("UserService-aop-test")
16public class AddressServiceSpy extends AddressService{
17    @Getter
18    private AddressService spyDelegate;
19     
20    @Autowired
21    public AddressServiceSpy(AddressDao addressDao) {
22        super(null);
23        spyDelegate = Mockito.spy(new AddressService(addressDao));
24    }
25     
26    public String getAddressForUser(String userName){
27        return spyDelegate.getAddressForUser(userName);
28    }
29}

As we can see this spy is very similar to AddressDaoMock. But in this case real bean is using constructor injection to autowire its dependency. Therefore we’ll need to define non-default constructor and do constructor injection also. But we wouldn’t pass injected dependency into parent constructor.

To enable spying on real object, we construct new instance with all the dependencies, wrap it into Mockito spy instance and store it into spyDelegate property. We expect call of method getAddressForUser() during test, therefore we delegate this call to spyDelegate. This property can be accessed in test via getter defined by Lombok’s @Getter annotation.

Test itself would look like this:

01@ActiveProfiles({"UserService-aop-test""aop"})
02@RunWith(SpringJUnit4ClassRunner.class)
03@SpringApplicationConfiguration(AopApplication.class)
04public class UserServiceAopITest {
05    @Autowired
06    private UserService userService;
07 
08    @Autowired
09    private AddressService addressService;
10     
11    @Test
12    public void testGetUserDetails() {
13        // GIVEN
14        AddressServiceSpy addressServiceSpy = (AddressServiceSpy) addressService;
15 
16        // WHEN
17        String actualUserDetails = userService.getUserDetails("john");
18   
19        // THEN
20        Assert.assertEquals("User john, 3 Dark Corner", actualUserDetails);
21        Mockito.verify(addressServiceSpy.getSpyDelegate()).getAddressForUser("john");
22    }
23}

It is very straight forward. Profile UserService-aop-test ensures that AddressServiceSpy will be scanned. Profile aopensures same for AddressLogger aspect. When we autowire testing object UserService and its dependency AddressService, we know that we can cast it to AddressServiceSpy and verify the call on its spyDelegate property after calling the testing method.

Fake Spring bean proxied by Spring AOP

It is obvious that delegating calls into Mockito mocks or spies complicates the testing. These patterns are often overkill if we simply need to fake the logic. We can use such fake in that case:

1@Primary
2@Repository
3@Profile("AddressService-aop-fake-test")
4public class AddressDaoFake extends AddressDao{
5    public String readAddress(String userName) {
6        return userName + "'s address";
7    }
8}

and used it for testing this way:

01@ActiveProfiles({"AddressService-aop-fake-test""aop"})
02@RunWith(SpringJUnit4ClassRunner.class)
03@SpringApplicationConfiguration(AopApplication.class)
04public class AddressServiceAopFakeITest {
05    @Autowired
06    private AddressService addressService;
07 
08    @Test
09    public void testGetAddressForUser() {
10        // GIVEN - Spring context
11  
12        // WHEN
13        String actualAddress = addressService.getAddressForUser("john");
14  
15        // THEN  
16        Assert.assertEquals("john's address", actualAddress);
17    }
18}

I don’t think this test needs explanation.

Reference:How to mock Spring bean (version 2) from our JCG partner Lubos Krnac at the Lubos Krnac Java blogblog.


출처: http://www.javacodegeeks.com/2016/01/mock-spring-bean-version-2.html

반응형
Comments