Spring Boot Testing: Complete Guide

Spring Boot Testing: Complete Guide

Spring Boot's testing support is one of its strongest features. Between JUnit 5, Mockito, AssertJ, and a suite of purpose-built test slice annotations, you get a layered testing strategy that lets you write fast unit tests, focused slice tests, and full integration tests — all within the same project.

This guide covers the full picture: what each layer tests, how to configure it, and where the performance trade-offs land.

Testing Layers in Spring Boot

Spring Boot applications typically follow three testing layers:

  • Unit tests — test a single class in isolation, no Spring context
  • Slice tests — load a minimal Spring context scoped to one layer (web, persistence, etc.)
  • Integration tests — load the full application context and test end-to-end behavior

Each layer has a different speed and confidence trade-off. Unit tests run in milliseconds; full integration tests can take seconds. A healthy test suite uses all three.

Test Dependencies: spring-boot-starter-test

Add the test starter to your pom.xml or build.gradle:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

This single dependency pulls in everything you need:

  • JUnit 5 (Jupiter) — the test runner
  • Mockito — mocking framework
  • AssertJ — fluent assertions
  • Hamcrest — matcher library
  • MockMvc — Spring MVC test support
  • JsonPath — JSON response assertions

No manual version management needed — Spring Boot's BOM pins all versions.

Unit Tests: No Spring Context

For pure business logic, skip the Spring context entirely. Use Mockito directly:

class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @InjectMocks
    private OrderService orderService;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void shouldCalculateTotalWithDiscount() {
        Order order = new Order(List.of(
            new LineItem("widget", 10.00, 3)
        ));
        when(orderRepository.findById(1L)).thenReturn(Optional.of(order));

        BigDecimal total = orderService.calculateTotal(1L, 0.10);

        assertThat(total).isEqualByComparingTo("27.00");
    }
}

These tests have zero Spring overhead. Run thousands of them in seconds.

@SpringBootTest: Full Integration Tests

@SpringBootTest loads the entire application context — all beans, configurations, and auto-configurations:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderApiIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void shouldCreateOrderAndReturnId() {
        CreateOrderRequest request = new CreateOrderRequest("customer-1", List.of(
            new LineItemRequest("widget", 3)
        ));

        ResponseEntity<OrderResponse> response = restTemplate.postForEntity(
            "/api/orders", request, OrderResponse.class
        );

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody().getId()).isNotNull();
    }
}

WebEnvironment.RANDOM_PORT starts a real embedded server on an available port. Use MOCK when you want MockMvc instead of a live server.

Full context tests are the slowest. Use them for critical paths and cross-cutting concerns — not for every endpoint variation.

@WebMvcTest: Controller Layer Only

@WebMvcTest loads only the web layer: controllers, filters, @ControllerAdvice, and MVC configuration. Service and repository beans are not created — you must mock them:

@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private OrderService orderService;

    @Test
    void shouldReturn201WhenOrderCreated() throws Exception {
        OrderResponse mockResponse = new OrderResponse(42L, "PENDING");
        when(orderService.createOrder(any())).thenReturn(mockResponse);

        mockMvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"customerId": "c1", "items": [{"sku": "widget", "qty": 3}]}
                    """))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.id").value(42))
            .andExpect(jsonPath("$.status").value("PENDING"));
    }

    @Test
    void shouldReturn400WhenRequestBodyInvalid() throws Exception {
        mockMvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{}"))
            .andExpect(status().isBadRequest());
    }
}

@WebMvcTest starts much faster than @SpringBootTest because it skips the persistence layer entirely. Use it for testing request mapping, validation, serialization, and error handling.

@DataJpaTest: Repository Layer Only

@DataJpaTest configures an in-memory H2 database, scans @Entity classes, and loads JPA repositories. No web layer, no services:

@DataJpaTest
class OrderRepositoryTest {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void shouldFindOrdersByCustomerId() {
        entityManager.persist(new Order("customer-1", OrderStatus.PENDING));
        entityManager.persist(new Order("customer-1", OrderStatus.SHIPPED));
        entityManager.persist(new Order("customer-2", OrderStatus.PENDING));
        entityManager.flush();

        List<Order> orders = orderRepository.findByCustomerId("customer-1");

        assertThat(orders).hasSize(2);
        assertThat(orders).allMatch(o -> o.getCustomerId().equals("customer-1"));
    }
}

By default, each test runs in a transaction that rolls back after the test — your database stays clean between tests without manual teardown.

@MockBean and @SpyBean

When Spring context tests need a real bean replaced with a mock, use @MockBean. It registers a Mockito mock into the application context:

@SpringBootTest
class NotificationTest {

    @MockBean
    private EmailService emailService;  // replaces the real bean in context

    @Autowired
    private OrderService orderService;

    @Test
    void shouldSendConfirmationEmailOnOrderCreation() {
        orderService.createOrder(validRequest());

        verify(emailService).sendOrderConfirmation(
            argThat(email -> email.getTo().equals("customer@example.com"))
        );
    }
}

@SpyBean wraps the real bean with a Mockito spy — the real method runs unless you explicitly stub it:

@SpyBean
private AuditService auditService;  // real bean, but you can verify calls or stub selectively

Use @MockBean when you want full control. Use @SpyBean when you want real behavior with the ability to assert calls or override specific methods.

TestRestTemplate and WebTestClient

For HTTP-level tests against a running server, Spring Boot provides two clients.

TestRestTemplate works with @SpringBootTest(webEnvironment = RANDOM_PORT) and handles cookies, follows redirects, and suppresses error-on-4xx:

@Autowired
private TestRestTemplate restTemplate;

@Test
void shouldReturnOrderDetails() {
    ResponseEntity<OrderResponse> response = restTemplate.getForEntity(
        "/api/orders/1", OrderResponse.class
    );
    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}

WebTestClient is the reactive alternative and also works for non-reactive apps in Spring Boot 3:

@Autowired
private WebTestClient webTestClient;

@Test
void shouldReturnOrderList() {
    webTestClient.get().uri("/api/orders")
        .exchange()
        .expectStatus().isOk()
        .expectBodyList(OrderResponse.class)
        .hasSize(3);
}

@TestConfiguration and Test Profiles

For test-specific beans, use @TestConfiguration. It supplements (rather than replaces) the main application configuration:

@TestConfiguration
class TestSecurityConfig {

    @Bean
    @Primary
    public SecurityService securityService() {
        return new NoOpSecurityService();  // bypass auth in tests
    }
}

For test-specific properties, create src/test/resources/application-test.properties:

spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.hibernate.ddl-auto=create-drop
app.email.enabled=false
app.external-api.base-url=http://localhost:8089

Activate with @ActiveProfiles("test") on your test class. You can also use @SpringBootTest(properties = {...}) for inline overrides when a full profile is overkill.

Testcontainers: Real Database Testing

H2 is fast but it's not production. Testcontainers spins up real Docker containers during tests:

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class OrderRepositoryContainerTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");

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

    @Autowired
    private OrderRepository orderRepository;

    @Test
    void shouldPersistAndRetrieveOrder() {
        Order saved = orderRepository.save(new Order("customer-1", OrderStatus.PENDING));
        Optional<Order> found = orderRepository.findById(saved.getId());

        assertThat(found).isPresent();
        assertThat(found.get().getCustomerId()).isEqualTo("customer-1");
    }
}

@DynamicPropertySource wires the container's dynamic port and credentials into the Spring context before it starts. Use Testcontainers when you need to test database-specific queries, constraints, or migrations that H2 won't replicate accurately.

Test Slices vs Full Context: Performance Considerations

Loading the full Spring context takes time — often 5–15 seconds per test class on a real application. Test slices exist to avoid this penalty:

Annotation Context Scope Startup Cost
@WebMvcTest Web layer only Low
@DataJpaTest JPA layer only Low
@WebFluxTest WebFlux layer only Low
@JsonTest JSON serialization only Very low
@SpringBootTest Full application High

Spring Boot caches application contexts across tests. Tests that share the same context configuration will reuse the already-started context — which is why mixing @MockBean with integration tests can hurt performance: each unique @MockBean combination creates a new context.

Best practice: group tests that share the same mock configuration into the same class. Reserve @SpringBootTest for smoke tests and cross-layer integration scenarios. Run slice tests for everything else — they give you 90% of the confidence at a fraction of the startup cost.

A practical split for most projects:

  • 70% unit tests (no Spring context)
  • 25% slice tests (@WebMvcTest, @DataJpaTest)
  • 5% full integration tests (@SpringBootTest)

This keeps the test suite fast enough to run on every commit without developers skipping it.

Test the Full User Journey

Spring Boot testing covers your backend. For end-to-end browser tests that verify the complete user experience, HelpMeTest generates and runs UI tests automatically with 24/7 monitoring — starting free.

Start testing free →

Read more