Mastodon

Spring Test Slices

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 …

@Controller
public class PersonController {
 
    private final PersonService personService;
 
    @Autowired
    public PersonController(PersonService personService) {
        this.personService = personService;
    }
 
    @RequestMapping("/persons")
    public ResponseEntity<List<Person>> listPersons() {
 
        return new ResponseEntity<>(personService.listAllPersons(), HttpStatus.OK);
    }
 
}

… which uses a PersonService …

public interface PersonService {
    List<Person> listAllPersons();
}

… and its implementation PersonServiceImpl …

@Service
public class PersonServiceImpl implements PersonService {
 
    private final PersonRepository personRepository;
 
    public PersonServiceImpl(PersonRepository personRepository) {
        this.personRepository = personRepository;
    }
 
    @Override
    public List<Person> listAllPersons() {
        return personRepository.findAll();
    }
}

… which uses the PersonRepository:

@Repository
public interface PersonRepository extends JpaRepository<Person, Integer> {
 
}

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:

@org.springframework.boot.test.context.SpringBootTest
class SpringBootTest {
 
    @Autowired
    private PersonController personController;
 
    @Test
    void controllerCall() {
        personController.listPersons();
    }
}

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.

@SpringBootTest
@AutoConfigureMockMvc
class AutoConfigureMockMvcTest {
 
    @Autowired
    private PersonController personController;
 
    @Test
    void controllerCall() {
        personController.listPersons();
    }
}

@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:

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

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:

@org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
class DataJpaTest {
 
    @Autowired
    private TestEntityManager entityManager;
 
    @Autowired
    private PersonRepository personRepository;
 
    @Test
    void testExample() {
        Person person = new Person();
        person.setName("Paul");
        this.entityManager.persist(person);
        Person reloadedPerson = personRepository.findAll().get(0);
        assertEquals("Paul", reloadedPerson.getName());
    }
}

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.