Testing Dependencies
Cover spring-boot-starter-test, spring-security-test for secured endpoints, and Testcontainers for real database integration tests in Docker.
Introduction
Testing a Spring Boot application well requires more than a bare JUnit dependency: you need to load the application context, mock security, and often talk to a real database to catch bugs that an in-memory fake would miss. This lesson covers the three dependencies that cover that full range, from a single unit test to a full integration test running against a real containerized database.
- What spring-boot-starter-test bundles together and why.
- How @SpringBootTest loads a real application context for integration tests.
- How spring-security-test's @WithMockUser tests secured endpoints without a real login.
- How Testcontainers gives you a real, disposable database for integration tests.
spring-boot-starter-test
This single starter bundles everything most projects need for testing: JUnit 5 for writing and running tests, Mockito for mocking dependencies, AssertJ for fluent, readable assertions, and Spring Test for loading application contexts in integration tests.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope></dependency>@SpringBootTestclass OrderServiceIntegrationTest {
@Autowired private OrderService orderService;
@MockBean private PaymentGateway paymentGateway;
@Test void placesOrderWhenPaymentSucceeds() { when(paymentGateway.charge(any(), anyDouble())).thenReturn(true);
Order order = orderService.placeOrder("cust-1", 49.99);
assertThat(order.getStatus()).isEqualTo(OrderStatus.CONFIRMED); verify(paymentGateway).charge("cust-1", 49.99); }}Click Run to see what this code prints.
Testing Secured Endpoints
When a controller is protected by Spring Security, a plain MockMvc call gets rejected before it ever reaches your code. spring-security-test's @WithMockUser simulates an authenticated user for the duration of a single test, without needing a real login flow or a token.
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope></dependency>@SpringBootTest@AutoConfigureMockMvcclass OrderControllerSecurityTest {
@Autowired private MockMvc mockMvc;
@Test void anonymousRequestIsRejected() throws Exception { mockMvc.perform(get("/api/orders")) .andExpect(status().isUnauthorized()); }
@Test @WithMockUser(username = "alice", roles = "CUSTOMER") void authenticatedRequestSucceeds() throws Exception { mockMvc.perform(get("/api/orders")) .andExpect(status().isOk()); }}Testcontainers: Real Databases in Docker
An in-memory H2 database is fast but does not behave exactly like PostgreSQL or MySQL — different SQL dialects, different constraint behavior, different edge cases. Testcontainers spins up the real database in a Docker container just for the test run, then tears it down automatically, so your integration tests run against the same engine production uses.
<dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <scope>test</scope></dependency><dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <scope>test</scope></dependency>@Testcontainers@SpringBootTestclass OrderRepositoryIntegrationTest {
@Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@DynamicPropertySource static void configureDataSource(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 savesAndReadsBackAnOrder() { Order saved = orderRepository.save(new Order("cust-1", 49.99));
Optional<Order> found = orderRepository.findById(saved.getId());
assertThat(found).isPresent(); assertThat(found.get().getTotal()).isEqualTo(49.99); }}Click Run to see what this code prints.
Testcontainers requires a working Docker (or Docker-compatible) runtime on the machine running the tests — this includes CI. Without it, tests annotated with @Testcontainers will fail to start the container.
Choosing the Right Layer
| Dependency | Scope | When to Use |
|---|---|---|
| spring-boot-starter-test | Unit + integration | Baseline for almost every test in the project |
| spring-security-test | Security slice | Any test that hits an endpoint protected by Spring Security |
| testcontainers (junit-jupiter + postgresql) | Full integration | Repository or query behavior that must match production database behavior exactly |
Common Mistakes
- Using @SpringBootTest for every single test, which loads the full context and makes the suite slow — prefer @WebMvcTest or @DataJpaTest slices where possible.
- Testing security rules against H2 instead of confirming they also hold with real authentication via @WithMockUser.
- Relying only on an in-memory database and discovering dialect-specific bugs for the first time in production.
- Forgetting Docker must be running for Testcontainers-based tests to pass, especially in CI pipelines.
Frequently Asked Questions
No — there are modules for MySQL, MongoDB, Redis, Kafka, Elasticsearch, and many other services, all following the same container-lifecycle pattern.
No, it fabricates an Authentication object directly in the Spring Security context for the duration of the test — no database row is created or required.
Yes, as long as the CI runner has Docker available, which GitHub-hosted runners do by default.
Summary
spring-boot-starter-test covers the everyday unit and integration testing baseline, spring-security-test lets you assert access rules without a real login, and Testcontainers closes the gap between tests and production by running against the real database engine. Next, we turn to observing a running application through Actuator and metrics.