Today, a colleague asked me to help him write a REST API integration test. We use Spring’s MockMvc API to test the REST API. The application uses MongoDB with Spring Data MongoDB. The application uses both MongoTemplate and Mongo based repositories for working with MongoDB. To make tests work independent of MongoDB, we mock Spring MongoDB repository interfaces.
Let’s suppose you are testing a REST API resource class shown below.
import java.net.URI; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(path = "/api/users") public class UserResource { @Autowired private UserRepository userRepository; @PostMapping public ResponseEntity<Void> create(CreateUserRequest request){ User user = userRepository.save(request.toUser()); HttpHeaders headers = new HttpHeaders(); headers.setLocation(URI.create("/api/users/"+user.getId())); return new ResponseEntity<>(headers, HttpStatus.CREATED); } }
The code shown above is an example resource class implemented using Spring MVC.
- We used
@RestController
annotation to mark the class as REST API controller.@RestController
annotation applies@ResponseBody
semantics so you don’t have to manually add it. - Next, we used Spring MongoDB repository interface
UserRepository
to save user to MongoDB. - Finally, we created
ResponseEntity
object and returned it back to the user.
The above code can be easily tested in isolation by mocking the UserRepository interface. We will use Spring Boot @MockBean annotation to mock out UserRepository
as shown below. @MockBean
is Mockito @Mock
wrapped in Spring clothes.
import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @RunWith(SpringRunner.class) @AutoConfigureMockMvc public class UserResourceTests { @Autowired private MockMvc mockMvc; @MockBean private UserRepository userRepository; @Test public void should_create_a_user() throws Exception { String json = "{\"username\":\"shekhargulati\",\"name\":\"Shekhar Gulati\"}"; when(userRepository.save(Mockito.any(User.class))).thenReturn(new User("123")); this.mockMvc .perform(post("/api/users").contentType(MediaType.APPLICATION_JSON).content(json)) .andDo(print()) .andExpect(status().isCreated()) .andExpect(header().string("Location", "/api/users/123")); } }
If you have written unit tests using Mockito’s @Mock annotation then you will find that @MockBean is very similar to it. The use case of @MockBean is integration test cases. It allows you to mock a specific bean in the dependency graph. Spring testing support with override the real bean with the mock created using Mockito. This is the reason we used Mockito’s when
API to set our expectation.
Testing MongoTemplate
If your application only uses Spring Data MongoDB repository interfaces then you can use @MockBean to mock them. It become interesting when your code uses both MongoTemplate and repository interfaces. Let’s suppose we have another resource class that create users using MongoTemplate as shown below.
@RestController @RequestMapping(path = "/api/users") public class UserResource { @Autowired private MongoTemplate mongoTemplate; @PostMapping public ResponseEntity<Void> create(@RequestBody CreateUserRequest request) { User user = this.mongoTemplate.findOne( Query.query(Criteria.where("username").is(request.username)), User.class ); if (user != null) { return new ResponseEntity<>(HttpStatus.CONFLICT); } mongoTemplate.save(request.toUser(), "user"); return new ResponseEntity<>(HttpStatus.CREATED); } }
The obvious way we can write test for UserResource is by mocking the MongoTemplate
using the @MockBean
annotation as shown below.
@SpringBootTest @RunWith(SpringRunner.class) @AutoConfigureMockMvc public class UserResourceTests { @Autowired private MockMvc mockMvc; @MockBean private MongoTemplate mongoTemplate; @Test public void should_create_a_user() throws Exception { String json = "{\"username\":\"shekhargulati\",\"name\":\"Shekhar Gulati\"}"; when(mongoTemplate.findOne(Mockito.any(Query.class), Mockito.eq(User.class))).thenReturn(new User("123")); this.mockMvc .perform(post("/api/users").contentType(MediaType.APPLICATION_JSON).content(json)) .andDo(print()) .andExpect(status().isCreated()); verify(mongoTemplate).save(Mockito.any(User.class)); } }
But, when you will run the test case you will be greeted by NullPointerException
. Yes, NullPointerException!. The stacktrace is shown below.
Caused by: java.lang.NullPointerException at org.springframework.data.mongodb.repository.support.MongoRepositoryFactory.<init>(MongoRepositoryFactory.java:73) at org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean.getFactoryInstance(MongoRepositoryFactoryBean.java:104) at org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean.createRepositoryFactory(MongoRepositoryFactoryBean.java:88) at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet(RepositoryFactoryBeanSupport.java:248) at org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean.afterPropertiesSet(MongoRepositoryFactoryBean.java:117) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1687) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1624)
The reason test threw NullPointerException is that Spring uses MongoTemplate internally to provide implementation of your repository interfaces. One solution could be to not mock both repository interface and MongoTemplate. But that solution would not scale when your application has more than one Spring MongoDB repository. You will have to mock all the repositories each time you have to mock MongoTemplate.
@SpyBean to the rescue
Spy wraps the real bean but allows you to verify method invocation and mock individual methods without affecting any other method of the real bean. So, by making MongoTemplate a SpyBean we can mock only the methods we want to mock in our test case and leave the others untouched.
The modified test case with @SpyBean usage is shown below.
@SpringBootTest @RunWith(SpringRunner.class) @AutoConfigureMockMvc public class UserResourceTests { @Autowired private MockMvc mockMvc; @SpyBean private MongoTemplate mongoTemplate; @Test public void should_create_a_user() throws Exception { String json = "{\"username\":\"shekhargulati\",\"name\":\"Shekhar Gulati\"}"; doReturn(null) .when(mongoTemplate).findOne(Mockito.any(Query.class), Mockito.eq(User.class)); doNothing().when(mongoTemplate).save(Mockito.any(User.class)); this.mockMvc .perform(post("/api/users").contentType(MediaType.APPLICATION_JSON).content(json)) .andDo(print()) .andExpect(status().isCreated()); verify(mongoTemplate).save(Mockito.any(User.class)); } }
In the code example shown above, we did the following:
- We made
MongoTemplate
a spy by annotating it with@SpyBean
annotation. This does not override the Spring application context with a mock bean. So, repositories will keep working. - Next, we have to set expectations using the
doReturn
syntax. If you do the usual when syntax, then the real bean will call the method. Hence, it will throw the exception. - To mock the void method
mongoTemplate.save()
, we useddoNothing
API of Mockito. - Finally, we verified that save call was made using
verify
Mockito method.
Thanks for your clear post.
It works but I don’t know if I will use this annotation.
I even don’t know if we should use it.
By Using SpyBean, the mongoTemplate will join the real database to play the query we ask to repository layer.
So if you don’t have an empty DB for testing, this test will take a looooonngg time (it’s my case).
Also, unit testing has to be developped to avoid all interaction with external world that is not the case by using SpyBean annotation. So I think it’s not the best approach.
Regards.
Logan.
Thanks! I was getting a slightly different error: Failed to load ApplicationContext
But this solution still worked for me.
hello I want to know how do you deal with “Failed to load ApplicationContext” during use @SpyBean Thanks