Spring Boot provides excellent testing support at every layer — unit tests with mocked dependencies, slice tests that load only a relevant slice of the application context, and full integration tests. Knowing which test type to use for which scenario avoids the common trap of testing everything with full @SpringBootTest.

Key Points

  • @SpringBootTest: loads full application context — use for integration tests; slow, use sparingly
  • @WebMvcTest(Controller.class): loads only Spring MVC layer (controllers, filters, exception handlers) — fast, use for controller tests
  • @DataJpaTest: loads only JPA layer with in-memory H2 — fast, use for repository tests
  • @MockBean: replaces a bean in the application context with a Mockito mock — use in slice tests to isolate the layer
  • MockMvc: test MVC controllers without starting a real server — perform(), andExpect(), andDo(print())
  • Testcontainers: spin up real Docker containers (Postgres, Redis, Kafka) in tests — gold standard for integration testing
  • @Sql / @Sql(executionPhase = BEFORE_TEST_METHOD): run SQL scripts to set up or clean up test data
  • @Transactional on test class: each test method runs in a transaction that is rolled back after — no test data pollution
  • WireMock: mock external HTTP services — stub responses, verify calls, simulate errors and latency
AnnotationLoadsSpeedBest for
@SpringBootTestFull contextSlowEnd-to-end integration
@WebMvcTestMVC layer onlyFastController + HTTP mapping
@DataJpaTestJPA + DB onlyFastRepository queries
@JsonTestJackson onlyVery fastSerialization/deserialization
Plain unit testNothingFastestService / domain logic

Spring Boot testing: @WebMvcTest + MockMvc, @DataJpaTest + Testcontainers + Postgres, WireMock for external APIs

// Controller test — @WebMvcTest
@WebMvcTest(UserController.class)
class UserControllerTest {
    @Autowired MockMvc mvc;
    @MockBean UserService service;   // mock the dependency

    @Test
    void getUser_returnsOk() throws Exception {
        given(service.findById(1L))
            .willReturn(Optional.of(new UserResponse(1L, "Alice", "a@b.com")));

        mvc.perform(get("/api/v1/users/1").accept(APPLICATION_JSON))
           .andExpect(status().isOk())
           .andExpect(jsonPath("$.name").value("Alice"));
    }
}

// Repository test — real Postgres via Testcontainers
@DataJpaTest
@AutoConfigureTestDatabase(replace = NONE)   // don't replace with H2
@Testcontainers
class OrderRepositoryTest {

    @Container
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16").withReuse(true);

    @DynamicPropertySource
    static void props(DynamicPropertyRegistry r) {
        r.add("spring.datasource.url", postgres::getJdbcUrl);
        r.add("spring.datasource.username", postgres::getUsername);
        r.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired OrderRepository repo;

    @Test
    @Sql("classpath:test-data/orders.sql")
    void findByCustomer_returnsOrders() {
        var orders = repo.findByCustomer_EmailAndStatus("alice@test.com", PENDING);
        assertThat(orders).hasSize(2);
    }
}

// WireMock — mock external API
@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureWireMock(port = 0)   // random port, injected automatically
class PaymentGatewayTest {
    @Test
    void chargeCard_success() {
        stubFor(post(urlEqualTo("/v1/charges"))
            .willReturn(okJson("{"status":"succeeded"}")));
        var result = paymentClient.charge(100, "card_123");
        assertThat(result.status()).isEqualTo("succeeded");
    }
}

Real-World Example

Testcontainers with withReuse(true) and TESTCONTAINERS_REUSE_ENABLE=true env var keeps the container alive between test runs during local development — container startup only happens once per dev session, making the test suite feel fast even with real databases. In CI, each pipeline run gets a fresh container (no reuse), ensuring isolation.