Spring Boot Intermediate: Layers, Data and Testing
Testing Spring Boot Applications
In this lesson you will
- Choose between unit, slice and full-context tests
- Test a controller with @WebMvcTest, MockMvc and @MockitoBean
- Test a repository with @DataJpaTest
Introduction
Spring Boot makes tests easy, but there are several kinds, and choosing the right one keeps tests fast. Spring Boot 4 reorganised its test support into separate modules. I verified the following class and package names exist in the Spring Boot 4.1.2 release, but the test code below was not compiled or run here. If an import fails after an upgrade, check the Spring Boot testing documentation.
Dependencies
A project generated from Initializr already includes the test starters it needs, such as spring-boot-starter-webmvc-test and spring-boot-starter-data-jpa-test (with scope test in the build file). They bring JUnit 6, Mockito, AssertJ and Spring's test tools.
Kinds of tests
- Plain unit tests: create the class with
newand fake collaborators. No Spring at all, so they are very fast. - Slice tests: start only part of the application: the web layer or the JPA layer.
- Full-context tests:
@SpringBootTeststarts the entire application context. Use sparingly, because they are slower.
A unit test
Java
class BookServiceTest {
@Test
void throwsWhenMissing() {
BookRepository repository = org.mockito.Mockito.mock(BookRepository.class);
org.mockito.Mockito.when(repository.findById(99L)).thenReturn(java.util.Optional.empty());
BookService service = new BookService(repository);
org.junit.jupiter.api.Assertions.assertThrows(
BookNotFoundException.class, () -> service.get(99L));
}
}Constructor injection pays off here: no Spring container is needed.
Testing a controller with @WebMvcTest
@WebMvcTest starts only the web layer (controllers, JSON conversion, validation, exception handlers). You provide mock services.
Java
package com.example.demo.book;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(BookController.class)
class BookControllerTest {
@Autowired
MockMvc mockMvc;
@MockitoBean
BookService service;
@Test
void returnsBook() throws Exception {
given(service.get(1L)).willReturn(new Book("Dune", 1965));
mockMvc.perform(get("/books/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.title").value("Dune"));
}
@Test
void rejectsInvalidBody() throws Exception {
mockMvc.perform(post("/books")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\":\"\",\"year\":1000}"))
.andExpect(status().isBadRequest());
}
}Key points (names checked against the 4.1.2 artifacts):
@WebMvcTestis inorg.springframework.boot.webmvc.test.autoconfigurein Boot 4. (In Boot 3 it was in a different package.)@MockitoBeanreplaces the older@MockBeanand lives inorg.springframework.test.context.bean.override.mockito.MockMvcsends fake HTTP requests through the real Spring MVC machinery, with no network.jsonPath("$.title")checks a field in the JSON response.
Quick quiz
What does @WebMvcTest load?
Testing a repository with @DataJpaTest
Java
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
@DataJpaTest
class BookRepositoryTest {
@Autowired
BookRepository repository;
@Test
void findsBooksAfterYear() {
repository.save(new Book("Old", 1815));
repository.save(new Book("New", 1995));
var result = repository.findByYearGreaterThan(1900);
org.assertj.core.api.Assertions.assertThat(result)
.extracting(Book::getTitle)
.containsExactly("New");
}
}@DataJpaTest configures an in-memory database, scans entities and repositories and rolls back each test, so tests do not affect each other.
Full-context tests
Java
@SpringBootTest
class DemoApplicationTests {
@Test
void contextLoads() {}
}This generated test checks that the whole context starts, which catches wiring and configuration errors. For HTTP-level tests of the full app you can add @AutoConfigureMockMvc (also in the webmvc.test.autoconfigure package).
When Spring Security is present
With Spring Security on the classpath, @WebMvcTest secures endpoints, so requests return 401 or 403 unless you set up a user. Use @WithMockUser from the spring-security-test module for a logged-in user, and for state-changing requests add .with(csrf()) from SecurityMockMvcRequestPostProcessors when CSRF protection is on. The advanced course covers security.
Good test habits
- Name tests for the behaviour:
throwsWhenMissing. - Test failures and edges as well as happy paths.
- Keep most tests as unit or slice tests. They are fast.
- Do not test the framework. Test your rules.
- Run the tests in your build (
./mvnw test) and in continuous integration.
Try it yourself
Write a unit test for your service, a @WebMvcTest for the controller covering 200, 404 and 400, and a @DataJpaTest for one derived query. Run ./mvnw test.
Summary
- Prefer fast unit tests and slice tests, and use @SpringBootTest sparingly.
- @WebMvcTest with MockMvc and @MockitoBean tests controllers, and @DataJpaTest tests repositories.
- In Spring Boot 4 the test annotations moved to new packages, so check imports against your version.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.
Sources and further reading