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

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

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

Java
@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 @SpringBootTest for 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 @MockBean for 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 @DataJpaTest exercises 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 @DataJpaTest rolls back each test's transaction automatically, then manually deleting rows "just in case" — harmless but unnecessary.