Testing Spring Boot Apps
@WebMvcTest vs @DataJpaTest vs full @SpringBootTest, with a complete working example of each.
Slice tests vs a full application context
Loading the entire application for every test is correct for true end-to-end coverage, but slow — dozens of beans, a real (or embedded) database, the whole web stack, all starting up per test class. Spring Boot's test slices load only the beans relevant to one architectural layer, which is both faster and forces the test to depend only on what it's actually testing.
| Annotation | Loads | Use for |
|---|---|---|
@WebMvcTest |
Only the web layer — controllers, @ControllerAdvice, Jackson, filters — not @Service/@Repository beans |
Testing a controller's request/response handling, status codes, validation, JSON shape |
@DataJpaTest |
Only JPA-related beans — repositories, the EntityManager, an embedded/test database — not controllers or services |
Testing repository queries actually run against a real (test) database |
@SpringBootTest |
The entire application context, exactly as production would build it | Genuine end-to-end tests, or verifying multiple layers wired together correctly |
@WebMvcTest: testing the web layer in isolation
@WebMvcTest(BookController.class)
class BookControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private BookService bookService; // BookController's real dependency, replaced with a mock
@Test
void getBookReturnsJsonWithOkStatus() throws Exception {
when(bookService.findById(1L)).thenReturn(new Book(1L, "1984", "George Orwell"));
mockMvc.perform(get("/api/books/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.title").value("1984"))
.andExpect(jsonPath("$.author").value("George Orwell"));
}
@Test
void getUnknownBookReturns404() throws Exception {
when(bookService.findById(99L)).thenThrow(new BookNotFoundException(99L));
mockMvc.perform(get("/api/books/99"))
.andExpect(status().isNotFound());
}
}
No real BookService bean is created at all — @WebMvcTest only instantiates the DispatcherServlet machinery and the one controller named, and @MockBean supplies a stand-in for everything that controller depends on. MockMvc sends a simulated HTTP request through the real Spring MVC dispatch machinery (mapping, argument resolution, JSON serialization, exception handling) without starting a real HTTP server or port.
@DataJpaTest: testing the repository layer against a real database
@DataJpaTest
class BookRepositoryTest {
@Autowired
private BookRepository bookRepository;
@Autowired
private TestEntityManager entityManager; // a test-focused EntityManager, for setting up data directly
@Test
void findByAuthorReturnsMatchingBooks() {
entityManager.persist(new Book(null, "1984", "George Orwell"));
entityManager.persist(new Book(null, "Animal Farm", "George Orwell"));
entityManager.persist(new Book(null, "Brave New World", "Aldous Huxley"));
List<Book> orwellBooks = bookRepository.findByAuthor("George Orwell");
assertThat(orwellBooks).hasSize(2);
}
}
By default, @DataJpaTest swaps in an embedded, in-memory database (H2, if it's on the classpath) instead of your real configured DataSource, and wraps each test method in a transaction that's rolled back automatically at the end — so tests never leave data behind for each other, with no manual cleanup code needed. To test against the actual production database technology instead of an embedded stand-in, pair it with Testcontainers rather than relying on H2's SQL-dialect compatibility.
@SpringBootTest: full end-to-end
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class BookApiIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void creatingAndFetchingABookWorksEndToEnd() {
var createResponse = restTemplate.postForEntity(
"/api/books", new CreateBookRequest("Dune", "Frank Herbert"), Book.class
);
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
Long id = createResponse.getBody().id();
var getResponse = restTemplate.getForEntity("/api/books/" + id, Book.class);
assertThat(getResponse.getBody().title()).isEqualTo("Dune");
}
}
webEnvironment = RANDOM_PORT starts a real embedded server on a free port and TestRestTemplate makes genuine HTTP calls against it — the closest thing to testing exactly what a real client would experience, at the cost of being the slowest and heaviest of the three approaches.
Choosing between them
@WebMvcTest |
@DataJpaTest |
@SpringBootTest |
|
|---|---|---|---|
| Context size | Web layer only | JPA layer only | Everything |
| Speed | Fast | Fast | Slow |
| Real HTTP server? | No -- MockMvc simulates requests |
No | Only with webEnvironment set |
| Real database? | No | Embedded/test DB | Whatever is configured (or overridden) |
| Good for | Controller logic, status codes, JSON shape, validation | Query correctness | True end-to-end behavior, wiring across layers |
Common mistakes
- Reaching for
@SpringBootTestfor a test that's really only exercising one controller or one repository — a slice test runs faster and fails with a clearer, more localized cause when something breaks. - Forgetting
@MockBeanfor a controller's real dependencies inside@WebMvcTest— without it, the test fails to start because the service/repository beans that controller depends on were never loaded into this slice's narrower context. - Assuming
@DataJpaTestexercises your real production database — by default it swaps in an embedded database, which can behave subtly differently (SQL dialect, constraint enforcement) from your actual production database engine. - Not realizing
@DataJpaTestrolls back each test's transaction automatically, then manually deleting rows "just in case" — harmless but unnecessary.