Spring Boot Tutorial
Integration Tests with Testcontainers
Unit and slice tests prove that each layer works on its own. Integration tests prove that the layers work together — HTTP request to controller to service to database and back, including security, JSON, transactions, migrations and messaging — against real infrastructure.
Testcontainers starts real PostgreSQL, Redis, Kafka, RabbitMQ or any Docker image from your test code, and Spring Boot's @ServiceConnection wires them into your application automatically. This lesson builds full end-to-end tests with @SpringBootTest, Testcontainers 2 and the new RestTestClient, shares containers across tests, and reuses the same setup to run the application locally.
@SpringBootTest and Web Environments
@SpringBootTest starts the whole application context. With webEnvironment = RANDOM_PORT it also starts the embedded server on a free port, so tests send real HTTP requests. The default MOCK environment uses a mock servlet environment (combine it with @AutoConfigureMockMvc — Spring Boot 4 no longer adds MockMvc automatically). Full-context tests are slower, so keep them few and focused on important end-to-end flows.
RestTestClient
Spring Framework 7 adds RestTestClient, a fluent test client for servlet applications (the counterpart of WebTestClient). In Spring Boot 4 add spring-boot-starter-restclient-test and annotate the test with @AutoConfigureRestTestClient; the client is pre-configured with the random port. It replaces most uses of TestRestTemplate, which now also needs an explicit @AutoConfigureTestRestTemplate.
Testcontainers and @ServiceConnection
A @Container field declares a container; Testcontainers starts it before the tests. @ServiceConnection tells Spring Boot to derive connection details from it — datasource URL, Redis host, Kafka bootstrap servers — without any @DynamicPropertySource boilerplate. Supported out of the box: PostgreSQL, MySQL, MariaDB, Oracle, SQL Server, MongoDB, Redis, Kafka, RabbitMQ, Elasticsearch, Cassandra, Neo4j, ActiveMQ, Ollama (via Spring AI) and more.
Sharing Containers and Speed
Starting containers takes seconds. Declare containers as @Beans in a @TestConfiguration class and @Import it from each test: Spring's test context cache then reuses the same context and containers across all test classes with the same configuration. Avoid @DirtiesContext, which throws the cached context away.
Running the App Locally with the Same Containers
Create a TestApplication class in src/test/java that calls SpringApplication.from(Application::main).with(ContainersConfig.class).run(args). Running it starts your application with fresh containers, so new developers can run the whole system with zero local installation — only Docker.
Examples
Shared container configuration
@TestConfiguration(proxyBeanMethods = false)
public class ContainersConfig {
@Bean
@ServiceConnection
PostgreSQLContainer postgres() {
return new PostgreSQLContainer("postgres:17");
}
@Bean
@ServiceConnection(name = "redis")
GenericContainer<?> redis() {
return new GenericContainer<>("redis:8").withExposedPorts(6379);
}
}
(Any test that @Imports ContainersConfig shares the same running PostgreSQL and Redis containers through Spring's context cache.)
End-to-end HTTP test with RestTestClient
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-restclient-test</artifactId>
<scope>test</scope>
</dependency>
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
@Import(ContainersConfig.class)
class ProductApiIT {
@Autowired RestTestClient client;
@Autowired ProductRepository repository;
@BeforeEach
void clean() {
repository.deleteAll();
}
@Test
void createThenFetchProduct() {
client.post().uri("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.body(Map.of("name", "Hoodie", "price", 1299))
.headers(h -> h.setBasicAuth("admin", "admin123"))
.exchange()
.expectStatus().isCreated()
.expectHeader().exists("Location");
client.get().uri("/api/products?name=Hoodie")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.content[0].name").isEqualTo("Hoodie")
.jsonPath("$.content[0].price").isEqualTo(1299.0);
assertThat(repository.count()).isEqualTo(1);
}
@Test
void anonymousUsersCannotCreateProducts() {
client.post().uri("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.body(Map.of("name", "Cap", "price", 499))
.exchange()
.expectStatus().isUnauthorized();
}
}
Container postgres:17 started
Container redis:8 started
Tomcat started on port 54872 (http)
ProductApiIT
✔ createThenFetchProduct()
✔ anonymousUsersCannotCreateProducts()
Full-context test with MockMvc (no real server)
@SpringBootTest
@AutoConfigureMockMvc // required explicitly in Spring Boot 4
@Import(ContainersConfig.class)
class CheckoutFlowIT {
@Autowired MockMvcTester mvc;
@Test
@WithMockUser(username = "asha@webnest.in")
void checkoutReducesStockAndCreatesOrder() {
assertThat(mvc.post().uri("/api/checkout")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"items": [{"productId": 1, "quantity": 2}]}
""")
.with(csrf()))
.hasStatus(HttpStatus.CREATED)
.bodyJson()
.extractingPath("$.status").isEqualTo("PLACED");
}
}
CheckoutFlowIT > checkoutReducesStockAndCreatesOrder() PASSED
Running the application locally with the test containers
// src/test/java/com/webnest/shop/TestShopApplication.java
public class TestShopApplication {
public static void main(String[] args) {
SpringApplication.from(ShopApplication::main)
.with(ContainersConfig.class)
.run(args);
}
}
# Maven: run the app from the test classpath
./mvnw spring-boot:test-run
# Gradle
./gradlew bootTestRun
Container postgres:17 started
Container redis:8 started
Started TestShopApplication in 6.2 seconds
(No local PostgreSQL or Redis installation needed — only Docker.)
Common Mistakes
- Writing most tests as full @SpringBootTest integration tests, producing a slow suite that nobody runs locally.
- Using @DirtiesContext liberally, forcing Spring to restart contexts and containers for every class.
- Hard-coding container ports instead of letting @ServiceConnection supply the mapped port.
- Forgetting that Spring Boot 4 needs @AutoConfigureMockMvc or @AutoConfigureRestTestClient explicitly.
- Leaving data from one test in the database, making other tests order-dependent; clean up or use unique data.
Key Points to Remember
- @SpringBootTest(webEnvironment = RANDOM_PORT) runs the full application on a real port.
- RestTestClient (spring-boot-starter-restclient-test + @AutoConfigureRestTestClient) is the modern HTTP test client.
- @ServiceConnection wires Testcontainers into Spring Boot with no manual properties.
- Share containers through a @TestConfiguration so the context cache reuses them.
- SpringApplication.from(...).with(...) runs the app locally with the same containers.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.