CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl-labs/springboot-testing

Spring Boot testing — @WebMvcTest for controllers, @DataJpaTest for repositories, @SpringBootTest only for integration, MockMvc, @MockBean vs @Mock, AssertJ, @Transactional rollback, @ActiveProfiles, TestContainers

93

1.09x
Quality

89%

Does it follow best practices?

Impact

100%

1.09x

Average score across 5 eval scenarios

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files
name:
springboot-testing
description:
Spring Boot testing best practices — use WebMvcTest for controller-only tests, DataJpaTest for repository tests, SpringBootTest only for integration. Covers MockMvc, MockBean vs Mock placement, slice test annotations, AssertJ fluent assertions, Transactional rollback, Sql test data, TestContainers for real databases, and ActiveProfiles for test config.
keywords:
spring boot testing, junit, mockmvc, springboottest, webmvctest, datajpatest, mockbean, testcontainers, assertj, integration test, slice test, spring test, java api testing, activerprofiles
license:
MIT

Spring Boot Testing Best Practices

Write tests that are fast, isolated, and catch real bugs. Use the narrowest test slice that covers the behavior.


1. Test Slice Selection — Use the Narrowest Annotation

Spring Boot provides slice annotations that load only the beans needed for a specific layer. Do not use @SpringBootTest when a slice annotation will do — full context startup is slow and hides coupling bugs.

AnnotationWhat it loadsUse for
@WebMvcTest(Controller.class)Controller + MVC infrastructure onlyController request mapping, validation, serialization
@DataJpaTestJPA repositories + embedded DBRepository queries, entity mappings
@SpringBootTestFull application contextEnd-to-end integration, multi-layer flows

Controller tests with @WebMvcTest

@WebMvcTest loads only the web layer. You must mock service dependencies with @MockBean:

@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private OrderService orderService;

    @Test
    void createOrder_returnsCreated() throws Exception {
        OrderDto order = new OrderDto(1L, "Test Customer", List.of());
        when(orderService.createOrder(any())).thenReturn(order);

        mockMvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"customerName": "Test Customer", "items": []}
                    """))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.customerName").value("Test Customer"));
    }

    @Test
    void createOrder_rejectsEmptyName() throws Exception {
        mockMvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"customerName": "", "items": []}
                    """))
            .andExpect(status().isBadRequest());
    }

    @Test
    void getOrder_notFound() throws Exception {
        when(orderService.getOrder(99999L))
            .thenThrow(new OrderNotFoundException(99999L));

        mockMvc.perform(get("/api/orders/99999"))
            .andExpect(status().isNotFound());
    }
}

Why @WebMvcTest over @SpringBootTest + @AutoConfigureMockMvc:

  • Starts in ~1s instead of ~5-10s (no database, no full context)
  • Forces you to declare controller dependencies explicitly via @MockBean
  • Failures point directly at the controller layer

Repository tests with @DataJpaTest

@DataJpaTest configures an embedded database, scans for @Entity classes, and configures JPA repositories. Tests are @Transactional and auto-rollback after each test — no manual cleanup needed:

@DataJpaTest
class OrderRepositoryTest {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void findByCustomerName_returnsMatchingOrders() {
        Order order = new Order();
        order.setCustomerName("Alice");
        order.setStatus(OrderStatus.PENDING);
        entityManager.persistAndFlush(order);

        List<Order> found = orderRepository.findByCustomerName("Alice");

        assertThat(found).hasSize(1);
        assertThat(found.get(0).getCustomerName()).isEqualTo("Alice");
    }

    @Test
    void findByStatus_returnsEmpty_whenNoneMatch() {
        List<Order> found = orderRepository.findByStatus(OrderStatus.COMPLETED);

        assertThat(found).isEmpty();
    }
}

Key points:

  • TestEntityManager gives you persistAndFlush() for test data — avoids going through repository for setup
  • Auto-rollback means tests are isolated without @BeforeEach cleanup
  • Uses H2 by default; configure AutoConfigureTestDatabase(replace = Replace.NONE) to test against a real database

Integration tests with @SpringBootTest (only when needed)

Use @SpringBootTest when you need to test flows that cross multiple layers (controller -> service -> repository):

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class OrderIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void createAndRetrieveOrder() throws Exception {
        MvcResult result = mockMvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"customerName": "Integration Test", "items": [{"productId": 1, "quantity": 2}]}
                    """))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.id").exists())
            .andReturn();

        String id = JsonPath.read(
            result.getResponse().getContentAsString(), "$.id").toString();

        mockMvc.perform(get("/api/orders/" + id))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.customerName").value("Integration Test"));
    }
}

Use @Transactional on integration test classes to auto-rollback database changes. This eliminates the need for @BeforeEach cleanup and @DirtiesContext.


2. @MockBean vs @Mock — Placement Matters

AnnotationWhereEffect
@MockBeanOn fields in @WebMvcTest / @SpringBootTest classesReplaces the bean in the Spring context
@Mock + @InjectMocksOn fields in plain unit tests (no Spring)Mockito-only, no Spring context

Rule: Use @Mock for unit tests, @MockBean for Spring slice tests.

// Plain unit test — fast, no Spring context
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private PaymentService paymentService;

    @InjectMocks
    private OrderService orderService;

    @Test
    void processOrder_calculatesTotal() {
        // test pure business logic
    }
}

Never use @MockBean in a plain unit test — it starts a Spring context unnecessarily.


3. MockMvc vs WebTestClient

ToolUse whenStyle
MockMvc@WebMvcTest, @SpringBootTest with servlet stackSynchronous, .andExpect() chain
WebTestClient@SpringBootTest(webEnvironment = RANDOM_PORT), WebFluxReactive/fluent, .expectStatus() chain
// WebTestClient for running server tests
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderApiWebTest {

    @Autowired
    private WebTestClient webTestClient;

    @Test
    void getOrders_returnsOk() {
        webTestClient.get().uri("/api/orders")
            .exchange()
            .expectStatus().isOk()
            .expectBody()
            .jsonPath("$").isArray();
    }
}

Prefer MockMvc for controller slice tests. Use WebTestClient when testing with a running server or WebFlux.


4. AssertJ for Fluent Assertions

Always use AssertJ (assertThat) instead of JUnit's assertEquals/assertTrue. It is included with spring-boot-starter-test:

// BAD — JUnit assertions, poor failure messages
assertEquals(3, orders.size());
assertTrue(order.getStatus() == OrderStatus.COMPLETED);

// GOOD — AssertJ, readable and descriptive failures
assertThat(orders).hasSize(3);
assertThat(orders).extracting(Order::getStatus)
    .containsOnly(OrderStatus.PENDING, OrderStatus.COMPLETED);
assertThat(order.getTotal()).isCloseTo(BigDecimal.valueOf(29.99), within(BigDecimal.valueOf(0.01)));
assertThat(order.getCustomerName()).isEqualTo("Alice");

5. Test Data Setup with @Sql

Use @Sql to load test data from SQL scripts instead of manual entity creation:

@DataJpaTest
@Sql("/test-data/orders.sql")
class OrderRepositoryTest {

    @Autowired
    private OrderRepository orderRepository;

    @Test
    void findPendingOrders_returnsExpectedCount() {
        List<Order> pending = orderRepository.findByStatus(OrderStatus.PENDING);
        assertThat(pending).hasSize(3);
    }
}
-- src/test/resources/test-data/orders.sql
INSERT INTO orders (id, customer_name, status, total) VALUES (1, 'Alice', 'PENDING', 25.00);
INSERT INTO orders (id, customer_name, status, total) VALUES (2, 'Bob', 'PENDING', 30.00);
INSERT INTO orders (id, customer_name, status, total) VALUES (3, 'Carol', 'PENDING', 15.00);
INSERT INTO orders (id, customer_name, status, total) VALUES (4, 'Dave', 'COMPLETED', 40.00);

@Sql runs before each test method by default. Combined with @Transactional rollback, each test gets a clean dataset.


6. TestContainers for Real Database Tests

When you need to test against a real database (PostgreSQL, MySQL), use TestContainers:

@SpringBootTest
@Testcontainers
@Transactional
class OrderRepositoryPostgresTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

    @Autowired
    private OrderRepository orderRepository;

    @Test
    void nativeQuery_worksWithRealPostgres() {
        // Test Postgres-specific features (JSONB, full-text search, etc.)
    }
}

@ServiceConnection (Spring Boot 3.1+) auto-configures the datasource from the container — no manual property setup.

Add the dependency:

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

7. Test Configuration with @ActiveProfiles

Use @ActiveProfiles("test") to load test-specific configuration:

@SpringBootTest
@ActiveProfiles("test")
class OrderIntegrationTest { ... }
# src/test/resources/application-test.yml
spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true
logging:
  level:
    org.springframework.test: DEBUG

8. Avoid @DirtiesContext

@DirtiesContext destroys and recreates the Spring context after a test. This is extremely slow. Instead:

  • Use @Transactional on test classes for auto-rollback (preferred)
  • Use @Sql with executionPhase = AFTER_TEST_METHOD for cleanup
  • Use @BeforeEach to reset only what you need

Only use @DirtiesContext if a test modifies Spring beans themselves (e.g., changing configuration properties at runtime).


9. Setup — Dependencies

Spring Boot includes all testing dependencies via the starter. Ensure it is present:

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

This includes: JUnit 5, Mockito, AssertJ, Spring Test, MockMvc, JSONPath, Hamcrest.

Run tests: ./mvnw test or ./gradlew test


Checklist

Test slice selection:

  • @WebMvcTest(Controller.class) for controller-only tests
  • @DataJpaTest for repository tests
  • @SpringBootTest only for multi-layer integration tests
  • No full context loaded when a slice annotation suffices

Mocking:

  • @MockBean in Spring slice tests (@WebMvcTest, @SpringBootTest)
  • @Mock + @InjectMocks in plain unit tests (no Spring context)
  • Service dependencies explicitly declared via @MockBean in controller tests

Test isolation:

  • @Transactional on test classes for auto-rollback (not manual deleteAll())
  • @DirtiesContext used sparingly (only when beans are modified)
  • @Sql for repeatable test data setup

Assertions:

  • AssertJ assertThat() used for all assertions (not JUnit assertEquals)
  • MockMvc .andExpect() chains for HTTP response assertions
  • jsonPath() for JSON response validation

Configuration:

  • @ActiveProfiles("test") with test-specific application-test.yml
  • H2 in-memory database for fast tests (or TestContainers for real DB fidelity)
  • spring-boot-starter-test dependency present

Verifiers

  • springboot-controller-tests — Write tests for a Spring Boot order processing service
  • springboot-repository-tests — Write tests for a Spring Boot product catalog repository
  • springboot-integration-tests — Write integration tests for a Spring Boot user registration flow
  • springboot-service-unit-tests — Write unit tests for a Spring Boot payment processing service
  • springboot-test-config — Add test infrastructure to a Spring Boot project with PostgreSQL
Workspace
tessl-labs
Visibility
Public
Created
Last updated
Publish Source
CLI
Badge
tessl-labs/springboot-testing badge