Integration tests verify that different modules or services work together correctly. Unlike unit tests, they cross boundaries (Database, File System, Network) and are essential for catching "glue code" bugs.
The modern standard for integration testing is TestContainers. It allows you to spin up lightweight, throwaway instances of your real infrastructure (PostgreSQL, Redis, Kafka) inside Docker containers during the test run.
Benefits:
@Testcontainers
class UserRepositoryIT {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@Test
void shouldSaveAndRetrieveUser() {
// Database is running in a real Docker container
String jdbcUrl = postgres.getJdbcUrl();
UserRepository repo = new UserRepository(jdbcUrl);
repo.save(new User("bob", "bob@example.com"));
Optional<User> found = repo.findByUsername("bob");
assertTrue(found.isPresent());
assertEquals("bob@example.com", found.get().getEmail());
}
}
When your system depends on a 3rd party REST API (e.g., Stripe, Twilio), do not hit the real production/sandbox servers. Use WireMock to spin up a local HTTP server that returns pre-defined JSON responses.
@Test
void shouldHandlePaymentFailure() {
wireMockServer.stubFor(post(urlEqualTo("/v1/charges"))
.willReturn(aResponse()
.withStatus(402)
.withBody("{\"error\": \"insufficient_funds\"}")));
PaymentResult result = paymentClient.charge(100.0);
assertEquals(PaymentStatus.FAILED, result.status());
}
In microservice architectures, integration tests often break because the "Provider" changed their API and the "Consumer" didn't know. Contract Testing formalizes the agreement:
TRUNCATE on all tables to ensure absolute isolation.