Home / Spring Boot / Production and Testing / Testing Spring Boot Applications

Testing Spring Boot Applications

Spring Boot gives you fast unit tests, focused slice tests and full integration tests. Choose the smallest test that proves the behavior, because slice tests start much faster than full application tests.

Overview

Test typeAnnotationLoadsSpeed
Unit test@ExtendWith(MockitoExtension.class)Nothing from SpringVery fast
Web slice@WebMvcTestControllers, advice, converters onlyFast
Data slice@DataJpaTestJPA repositories and an in-memory DBFast
Full integration@SpringBootTestThe whole application contextSlower

spring-boot-starter-test brings JUnit 5, Mockito, AssertJ, Hamcrest, JsonPath and MockMvc. Note: in Spring Boot 4 the test annotations and auto-configure classes moved into modular packages, so check the imports of your version. The names and ideas stay the same.

1. Unit test of a service (no Spring)

@ExtendWith(MockitoExtension.class)
class ProductServiceTest {

    @Mock ProductRepository repository;
    @InjectMocks ProductService service;

    @Test
    void createRejectsDuplicateName() {
        when(repository.existsByNameIgnoreCase("Pen")).thenReturn(true);

        assertThatThrownBy(() -> service.create(new ProductRequest("Pen", BigDecimal.TEN)))
                .isInstanceOf(ConflictException.class)
                .hasMessageContaining("already exists");

        verify(repository, never()).save(any());
    }
}

2. Controller test with MockMvc

// Boot 3.x import: org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
@WebMvcTest(ProductController.class)
class ProductControllerTest {

    @Autowired MockMvc mvc;
    @MockitoBean ProductService service;          // @MockBean in older versions (removed in Boot 4)

    @Test
    void getReturnsProduct() throws Exception {
        when(service.get(1L)).thenReturn(new ProductResponse(1L, "Pen", BigDecimal.TEN));

        mvc.perform(get("/api/products/1"))
           .andExpect(status().isOk())
           .andExpect(jsonPath("$.name").value("Pen"));
    }

    @Test
    void createValidatesInput() throws Exception {
        mvc.perform(post("/api/products")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"name\":\"\",\"price\":-5}"))
           .andExpect(status().isBadRequest());
    }
}

3. Repository test

@DataJpaTest
class ProductRepositoryTest {

    @Autowired ProductRepository repository;

    @Test
    void findsByName() {
        repository.save(new Product("Pen", BigDecimal.TEN));
        assertThat(repository.existsByNameIgnoreCase("pen")).isTrue();
    }
}

4. Full integration test with a real database (Testcontainers)

@SpringBootTest
@AutoConfigureMockMvc
@Testcontainers
class ProductApiIT {

    @Container
    @ServiceConnection                               // Boot wires the datasource to the container automatically
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");

    @Autowired MockMvc mvc;

    @Test
    void createAndReadProduct() throws Exception {
        mvc.perform(post("/api/products").contentType(MediaType.APPLICATION_JSON)
                .content("{\"name\":\"Laptop\",\"price\":55000}"))
           .andExpect(status().isCreated());

        mvc.perform(get("/api/products"))
           .andExpect(status().isOk())
           .andExpect(jsonPath("$.content[0].name").value("Laptop"));
    }
}

Testcontainers runs real PostgreSQL, Kafka or Redis in Docker for the test. This finds problems that H2 cannot show, because H2 behaves differently from PostgreSQL.

Useful test settings

@ActiveProfiles("test")                                   // loads application-test.yml
@TestPropertySource(properties = "app.feature.enabled=true")
@DirtiesContext                                           // rebuild the context after this test (slow, use rarely)

How the Spring test context cache works

Test class A  @SpringBootTest                      ─► context #1 created (slow: 3 to 10 s)     stored in the cache
Test class B  @SpringBootTest  (same configuration) ─► context #1 REUSED (fast)
Test class C  @SpringBootTest @ActiveProfiles("x")  ─► different key ─► context #2 created
Test class D  @SpringBootTest + @MockitoBean Foo    ─► different key ─► context #3 created
Cache key = configuration classes + active profiles + properties (@TestPropertySource, properties=) + context initializers + mock/spy beans + web environment ...
Every different combination = one more application startup. Many variants make the build slow.

Dynamic properties for containers

@SpringBootTest
@Testcontainers
class RedisIT {
    @Container
    static GenericContainer<?> redis = new GenericContainer<>("redis:7").withExposedPorts(6379);

    @DynamicPropertySource
    static void redisProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.data.redis.host", redis::getHost);
        registry.add("spring.data.redis.port", () -> redis.getMappedPort(6379));
    }
}

More test slices

AnnotationLoadsUse for
@WebMvcTestControllers, advice, filters, converters (no services)HTTP behavior with MockMvc
@DataJpaTestEntities, repositories, embedded DB, rollbackQueries and mappings
@JdbcTestDataSource and JdbcTemplatePlain SQL code
@JsonTestJackson configurationJSON shape of DTOs
@RestClientTestRestClient and MockRestServiceServerOutgoing HTTP clients
@DataMongoTest / @DataRedisTestStore specific slicesData access with Testcontainers
@SpringBootTestEverythingEnd-to-end flows, few of them
@JsonTest
class ProductResponseJsonTest {
    @Autowired JacksonTester<ProductResponse> json;

    @Test
    void serializes() throws Exception {
        JsonContent<ProductResponse> result = json.write(new ProductResponse(1L, "Pen", new BigDecimal("10.50")));
        assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Pen");
        assertThat(result).extractingJsonPathNumberValue("$.price").isEqualTo(10.50);
    }
}

Test asynchronous behavior with Awaitility (no Thread.sleep)

@Test
void listenerProcessesEvent() {
    publisher.publish(new OrderPlaced("o-1", "c-1", BigDecimal.TEN));

    await().atMost(Duration.ofSeconds(5)).pollInterval(Duration.ofMillis(100))
           .untilAsserted(() -> assertThat(invoiceRepository.findByOrderId("o-1")).isPresent());
}

Test data builders keep tests readable

public class ProductBuilder {
    private String name = "Pen";
    private BigDecimal price = BigDecimal.TEN;
    public static ProductBuilder aProduct() { return new ProductBuilder(); }
    public ProductBuilder named(String name) { this.name = name; return this; }
    public ProductBuilder priced(String price) { this.price = new BigDecimal(price); return this; }
    public Product build() { return new Product(name, price, "general"); }
}
// Product laptop = aProduct().named("Laptop").priced("55000").build();

What makes tests flaky, and the fix

CauseFix
Thread.sleep and timingAwaitility with a condition
Shared database state between testsCleanup in @BeforeEach or rollback, unique test data
Current time and randomnessInject a Clock and a random source
Order dependenceEach test creates its own data
Real external servicesWireMock, MockRestServiceServer, Testcontainers
Ports in useRANDOM_PORT

Common mistake: Using @SpringBootTest for every test. The whole application starts each time and the test suite becomes very slow. Prefer unit tests and slice tests.

Interview questions

What is a test slice?

An annotation such as @WebMvcTest or @DataJpaTest that loads only the part of the application needed for that layer.

@Mock or @MockitoBean?

@Mock is plain Mockito for unit tests without Spring. @MockitoBean replaces a bean inside the Spring context for slice and integration tests.

Why Testcontainers instead of H2?

It tests against the same database engine you use in production, so SQL dialect and constraint differences are caught early.

Why are my Spring tests slow?

Usually because many different context configurations each start the application (different mocks, profiles, properties) and because @SpringBootTest is used where a slice or a unit test would do.

Should I mock the repository or use a real database?

Mock it in service unit tests to test logic. Use @DataJpaTest with a real database (Testcontainers) to test queries and mappings.