Spring Test Slices


Posted by Steven

In this article, I want to share what I learned about testing a Spring application without loading it completely. This can be done by using @DataJpaTest, @WebMvcTest, @SpringBootTest, @AutoConfigureMockMvc and @DataJpaTest. These annotations are part of "test slices" of Spring which allow to test certain beans of the application without loading other beans.

Setup

My simple example application (available on Github) has a PersonController ...

  1. @Controller
  2. public class PersonController {
  3.  
  4. private final PersonService personService;
  5.  
  6. @Autowired
  7. public PersonController(PersonService personService) {
  8. this.personService = personService;
  9. }
  10.  
  11. @RequestMapping("/persons")
  12. public ResponseEntity<List<Person>> listPersons() {
  13.  
  14. return new ResponseEntity<>(personService.listAllPersons(), HttpStatus.OK);
  15. }
  16.  
  17. }

... which uses a PersonService ...

  1. public interface PersonService {
  2. List<Person> listAllPersons();
  3. }

... and its implementation PersonServiceImpl ...

  1. @Service
  2. public class PersonServiceImpl implements PersonService {
  3.  
  4. private final PersonRepository personRepository;
  5.  
  6. public PersonServiceImpl(PersonRepository personRepository) {
  7. this.personRepository = personRepository;
  8. }
  9.  
  10. @Override
  11. public List<Person> listAllPersons() {
  12. return personRepository.findAll();
  13. }
  14. }

... which uses the PersonRepository:

  1. public interface PersonRepository extends JpaRepository<Person, Integer> {
  2.  
  3. }

There are five test classes, each demonstrating one of the test slices.

@SpringBootTest

Test classes annotated with this annotation will load the whole application context and also start a webserver , provided it's a web MVC application. This is a simple test class using @SpringBootTest:

  1. @org.springframework.boot.test.context.SpringBootTest
  2. class SpringBootTest {
  3.  
  4. @Autowired
  5. private PersonController personController;
  6.  
  7. @Test
  8. void controllerCall() {
  9. personController.listPersons();
  10. }
  11. }

On my machine, the Spring application context needed 6.9 seconds to load.

@AutoConfigureMockMvc

Only using @SpringBootTest will start a webserver. Adding @AutoConfigureMockMvc to the test class will prevent Spring from starting the server. However, a TestDispatcherServlet is being created which brings the startup time of the test to 7.5 seconds.

  1. @SpringBootTest
  2. @AutoConfigureMockMvc
  3. class AutoConfigureMockMvcTest {
  4.  
  5. @Autowired
  6. private PersonController personController;
  7.  
  8. @Test
  9. void controllerCall() {
  10. personController.listPersons();
  11. }
  12. }

@WebMvcTest

If only the web layer of the application should be tested, without starting any web server or even the persistence layer, @WebMvcTest can be used. In this example, the specific controller that should be tested is provided in the annotation, which speeds up start time:

  1. @org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest(PersonController.class)
  2. class WebMvcTest {
  3.  
  4. @Autowired
  5. MockMvc mockMvc;
  6.  
  7. // This service has to be mocked here, even when it is not used in the test. Using @WebMvcTest, only the specified
  8. // controller is created by Spring, not its dependencies. This is why they have to be provided here, as mock.
  9. @MockBean
  10. private PersonService personService;
  11.  
  12. @Test
  13. void controllerReturnsOK() throws Exception {
  14. this.mockMvc.perform(get("/persons")
  15. .accept(MediaType.APPLICATION_JSON))
  16. .andExpect(status().isOk());
  17. }
  18. }

On my machine, the setup of this test needed 3.7 seconds.

It's noteworthy that only the web layer and specifically only the given controller is set up automatically. Dependencies in the controller will have to be provided in the test class, for example with @MockBean.

@DataJpaTest

Kind of the opposite of @WebMvcTest is @DataJpaTest. This annotation only loads the persistence layer:

  1. @org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
  2. class DataJpaTest {
  3.  
  4. @Autowired
  5. private TestEntityManager entityManager;
  6.  
  7. @Autowired
  8. private PersonRepository personRepository;
  9.  
  10. @Test
  11. void testExample() {
  12. Person person = new Person();
  13. person.setName("Paul");
  14. this.entityManager.persist(person);
  15. Person reloadedPerson = personRepository.findAll().get(0);
  16. assertEquals("Paul", reloadedPerson.getName());
  17. }
  18. }

On my machine, the setup needed 4.9 seconds.

Summary

There are several build-in test layers in Spring that dramatically speed up integration tests.

Category: 
Share: