Testing Spring Boot Applications

Spring Boot ships an excellent testing toolkit (spring-boot-starter-test), bundling JUnit 5, AssertJ, Mockito, Spring's test framework, and JSON and HTTP testing utilities. The challenge isn't tooling. It's choosing the right level for each test: plain unit tests for business logic, test slices that load just one layer (web, JPA, JSON), and full @SpringBootTest integration tests against real infrastructure via Testcontainers.

Loading the entire application context for every test makes suites slow, while mocking everything hides integration bugs. A balanced pyramid, with fast unit tests at the base, focused slices in the middle, and a smaller set of realistic integration tests on top, gives confidence without 20-minute builds.

TL;DR

Quick Example

A controller slice test and a full integration test with Testcontainers:

@ServiceConnection wires the container's JDBC URL and credentials into Spring automatically, and Flyway migrations run against a real PostgreSQL.

Core Concepts

The Testing Pyramid for Spring

See unit testing and integration testing.

Unit Tests Without Spring

Services using constructor injection (dependency injection) are ordinary Java objects: instantiate them with in-memory fakes or Mockito mocks. These tests are the fastest and should cover most business rules.

Test Slices

By default, @DataJpaTest tries to replace your datasource with an embedded database. Pair it with Testcontainers and @AutoConfigureTestDatabase(replace = NONE) (or @ServiceConnection) to test real SQL behavior. See Spring Data JPA.

@SpringBootTest

Loads the complete application context:

Testcontainers and @ServiceConnection

Testcontainers starts real dependencies in Docker for tests (PostgreSQL, MySQL, Kafka, Redis, LocalStack, Elasticsearch). Spring Boot's @ServiceConnection automatically configures connection properties from the container, replacing manual @DynamicPropertySource wiring. Containers can be shared across test classes (static containers, or a shared configuration class imported with @Import) to avoid restart costs. Spring Boot can also run the same containers during local development (spring-boot-testcontainers with SpringApplication.from(...).with(...)).

Mocking Beans

@MockitoBean replaces a bean in the context with a Mockito mock, and @MockitoSpyBean wraps a real bean. Use them for boundaries you don't want to hit (payment gateways, email) or in slices to isolate the layer under test. Each distinct combination of mocked beans produces a different context cache key, so many variations lead to many context startups and slow suites.

Context Caching

Spring caches application contexts across test classes with identical configuration. Suites stay fast when tests share configuration. They slow down with many unique @MockitoBean sets, @DirtiesContext, differing properties, or profiles. Consolidate common setups in base classes or shared test configurations.

Best Practices

Test Behavior at the Right Layer

Validate business rules in unit tests, HTTP contracts (status codes, validation errors, JSON) in @WebMvcTest, queries and mappings in @DataJpaTest against a real database, and critical flows end to end with @SpringBootTest plus Testcontainers.

Use Real Databases for Persistence Tests

H2 differs from PostgreSQL and MySQL in SQL dialect, types, constraints, and locking. Testing against the same engine and version as production catches the bugs that matter.

Keep Tests Independent

Reset state between tests: @DataJpaTest rolls back by default, and for @SpringBootTest with a real server (where transactions don't roll back), clean tables or use unique data per test. Avoid ordering dependencies.

Test Security Rules Explicitly

Include tests for unauthenticated, unauthorized, and authorized access to sensitive endpoints, using spring-security-test helpers. See Spring Security.

Common Mistakes

@SpringBootTest for Everything

Loading the full context, with all beans, auto-configuration, and database connections, just to test a pricing calculation makes suites slow and brittle. Use plain unit tests or slices.

Proliferating Context Configurations

Standardize a few shared test configurations.

Asserting on Mocks Instead of Outcomes

Tests that only verify verify(repo).save(any()) pass even when the saved data is wrong. Prefer asserting observable outcomes (database state, responses, published events), and keep mock verification for true side-effect boundaries. See mocking and stubbing.

FAQ

What's the difference between @WebMvcTest and @SpringBootTest?

@WebMvcTest loads only the web layer (controllers, filters, JSON, security), with services mocked, so it's fast and focused on HTTP behavior. @SpringBootTest loads the entire application context, which is used for integration tests across layers, often with real infrastructure.

Should I use H2 for tests?

Prefer Testcontainers with the same database you run in production. H2's compatibility modes miss dialect differences, constraint behavior, JSON and array types, and locking semantics. Testcontainers with container reuse keeps runs fast enough for most teams.

What replaced @MockBean?

Spring Framework 6.2 / Spring Boot 3.4 introduced @MockitoBean and @MockitoSpyBean in the core test framework. @MockBean and @SpyBean are deprecated in Boot 3.4 and removed in later versions.

How do I speed up a slow Spring test suite?

Push logic into plain unit tests, use slices instead of full contexts, reduce distinct context configurations (so caching works), share Testcontainers across classes, run tests in parallel where safe, and profile which test classes start new contexts.

Related Topics

References