google-site-verification: google61fe8ba583a51912.html
Testcontainers for Integration Tests in Java

Introduction

Testcontainers is a Java library that provides lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container. It enables true integration testing by allowing tests to interact with real services in isolated containers.

Maven Dependencies

<!-- Testcontainers BOM --> <dependencyManagement> <dependencies> <dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers-bom</artifactId> <version>1.19.3</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <!-- Core Testcontainers --> <dependencies> <dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <scope>test</scope> </dependency> <!-- Database Modules --> <dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>mysql</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>mongodb</artifactId> <scope>test</scope> </dependency> <!-- Kafka --> <dependency> <groupId>org.testcontainers</groupId> <artifactId>kafka</artifactId> <scope>test</scope> </dependency> <!-- Selenium --> <dependency> <groupId>org.testcontainers</groupId> <artifactId>selenium</artifactId> <scope>test</scope> </dependency> <!-- Spring Boot Support --> <dependency> <groupId>org.testcontainers</groupId> <artifactId>spring-boot</artifactId> <scope>test</scope> </dependency> </dependencies>

Basic Testcontainers Setup

Simple Container Test

import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; @Testcontainers public class BasicContainerTest { @Container private static final GenericContainer<?> redisContainer = new GenericContainer<>(DockerImageName.parse("redis:7-alpine")) .withExposedPorts(6379); @Test void testRedisConnection() { String redisHost = redisContainer.getHost(); Integer redisPort = redisContainer.getFirstMappedPort(); // Use the actual container host and port String connectionString = String.format("redis://%s:%d", redisHost, redisPort); System.out.println("Redis connection: " + connectionString); // Test your Redis client implementation assertThat(redisContainer.isRunning()).isTrue(); } @Test void testMultipleContainers() { GenericContainer<?> nginxContainer = new GenericContainer<>("nginx:alpine") .withExposedPorts(80); nginxContainer.start(); try { assertThat(nginxContainer.isRunning()).isTrue(); } finally { nginxContainer.stop(); } } }

Database Integration Testing

PostgreSQL Integration Tests

import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import javax.sql.DataSource; import java.sql.Connection; import java.sql.ResultSet; import java.sql.Statement; import static org.assertj.core.api.Assertions.assertThat; @Testcontainers @SpringBootTest public class PostgresIntegrationTest { @Container private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); @DynamicPropertySource static void configureProperties(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 DataSource dataSource; @Test void testDatabaseConnection() throws Exception { try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT 1")) { assertThat(rs.next()).isTrue(); assertThat(rs.getInt(1)).isEqualTo(1); } } @Test void testDatabaseIsPostgreSQL() throws Exception { try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT version()")) { assertThat(rs.next()).isTrue(); String version = rs.getString(1); assertThat(version).contains("PostgreSQL"); } } } // JPA Repository Testing @DataJpaTest @Testcontainers class UserRepositoryTest { @Container private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine"); @DynamicPropertySource static void configureProperties(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 TestEntityManager entityManager; @Autowired private UserRepository userRepository; @Test void shouldSaveAndFindUser() { User user = User.builder() .username("testuser") .email("[email protected]") .build(); User saved = userRepository.save(user); assertThat(saved.getId()).isNotNull(); assertThat(userRepository.findByUsername("testuser")) .isPresent() .get() .extracting(User::getEmail) .isEqualTo("[email protected]"); } }

MySQL Integration Tests

@Testcontainers @SpringBootTest public class MySQLIntegrationTest { @Container private static final MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0") .withDatabaseName("testdb") .withUsername("test") .withPassword("test") .withInitScript("init-mysql.sql"); @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", mysql::getJdbcUrl); registry.add("spring.datasource.username", mysql::getUsername); registry.add("spring.datasource.password", mysql::getPassword); } @Autowired private DataSource dataSource; @Test void testMySQLSpecificFeatures() throws Exception { // Test MySQL-specific functionality } } // init-mysql.sql CREATE TABLE IF NOT EXISTS users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL UNIQUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );

MongoDB Integration Tests

import org.springframework.data.mongodb.core.MongoTemplate; import org.testcontainers.containers.MongoDBContainer; @Testcontainers @SpringBootTest public class MongoDBIntegrationTest { @Container private static final MongoDBContainer mongo = new MongoDBContainer("mongo:6.0") .withExposedPorts(27017); @DynamicPropertySource static void setProperties(DynamicPropertyRegistry registry) { registry.add("spring.data.mongodb.uri", mongo::getReplicaSetUrl); } @Autowired private MongoTemplate mongoTemplate; @Test void testMongoDBConnection() { // Verify connection works assertThat(mongoTemplate.getDb().getName()).isNotNull(); } @Test void shouldSaveAndRetrieveDocument() { UserDocument user = UserDocument.builder() .username("mongouser") .email("[email protected]") .build(); UserDocument saved = mongoTemplate.save(user); assertThat(saved.getId()).isNotNull(); assertThat(mongoTemplate.findById(saved.getId(), UserDocument.class)) .isNotNull() .extracting(UserDocument::getUsername) .isEqualTo("mongouser"); } }

Kafka Integration Testing

Kafka Testcontainers Setup

import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerRecord; import org.testcontainers.containers.KafkaContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.util.Collections; import java.util.Properties; @Testcontainers public class KafkaIntegrationTest { @Container private static final KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0")); private String getBootstrapServers() { return kafka.getBootstrapServers(); } @Test void testKafkaProduceConsume() { String topicName = "test-topic"; // Producer configuration Properties producerProps = new Properties(); producerProps.put("bootstrap.servers", getBootstrapServers()); producerProps.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); producerProps.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // Consumer configuration Properties consumerProps = new Properties(); consumerProps.put("bootstrap.servers", getBootstrapServers()); consumerProps.put("group.id", "test-group"); consumerProps.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); consumerProps.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); try (KafkaProducer<String, String> producer = new KafkaProducer<>(producerProps); KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps)) { // Produce message String testMessage = "Hello Kafka!"; producer.send(new ProducerRecord<>(topicName, "key1", testMessage)); producer.flush(); // Consume message consumer.subscribe(Collections.singletonList(topicName)); ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(10)); assertThat(records).isNotEmpty(); assertThat(records.iterator().next().value()).isEqualTo(testMessage); } } } // Spring Kafka Testing @SpringBootTest @Testcontainers class SpringKafkaTest { @Container private static final KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0")); @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers); } @Autowired private KafkaTemplate<String, String> kafkaTemplate; @Autowired private KafkaListenerEndpointRegistry registry; @Test void testSpringKafkaIntegration() throws Exception { String topic = "spring-test-topic"; String message = "Test message"; kafkaTemplate.send(topic, message); kafkaTemplate.flush(); // Wait for message to be processed Thread.sleep(2000); // Verify message was processed by your @KafkaListener } }

Complex Multi-Container Setups

Docker Compose Integration

import org.junit.jupiter.api.Test; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import java.io.File; @Testcontainers public class DockerComposeTest { @Container private static final DockerComposeContainer<?> compose = new DockerComposeContainer<>(new File("src/test/resources/docker-compose-test.yml")) .withExposedService("postgres_1", 5432) .withExposedService("redis_1", 6379) .withLocalCompose(true); @Test void testMultipleServices() { String postgresHost = compose.getServiceHost("postgres_1", 5432); Integer postgresPort = compose.getServicePort("postgres_1", 5432); String redisHost = compose.getServiceHost("redis_1", 6379); Integer redisPort = compose.getServicePort("redis_1", 6379); // Test connections to both services assertThat(postgresHost).isNotNull(); assertThat(redisHost).isNotNull(); } } // docker-compose-test.yml version: '3.8' services: postgres: image: postgres:15-alpine environment: POSTGRES_DB: testdb POSTGRES_USER: test POSTGRES_PASSWORD: test ports: - "5432" redis: image: redis:7-alpine ports: - "6379"

Custom Container Networks

@Testcontainers public class NetworkTest { private static final Network network = Network.newNetwork(); @Container private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withNetwork(network) .withNetworkAliases("postgres"); @Container private static final GenericContainer<?> app = new GenericContainer<>("my-app:latest") .withNetwork(network) .dependsOn(postgres) .withEnv("DATABASE_URL", "jdbc:postgresql://postgres:5432/testdb") .withExposedPorts(8080); @Test void testAppDatabaseConnection() { String appUrl = "http://" + app.getHost() + ":" + app.getFirstMappedPort(); // Test your application's HTTP endpoints // The app can connect to postgres using "postgres:5432" hostname } }

Spring Boot Testcontainers Support

@TestConfiguration with Testcontainers

@SpringBootTest @Testcontainers public class SpringBootIntegrationTest { @Container private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine"); @TestConfiguration static class TestConfig { @Bean @ServiceConnection // Spring Boot 3.1+ auto-configuration public PostgreSQLContainer<?> postgreSQLContainer() { return postgres; } } @Autowired private DataSource dataSource; @Test void testSpringDataJPA() { // Your repository tests with real database } } // Application properties for tests @TestConfiguration public class TestContainersConfiguration { @Bean @ServiceConnection public PostgreSQLContainer<?> postgreSQLContainer() { return new PostgreSQLContainer<>("postgres:15-alpine") .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); } @Bean @ServiceConnection public MongoDBContainer mongoDBContainer() { return new MongoDBContainer("mongo:6.0"); } }

Database Migration Testing

@Testcontainers @SpringBootTest public class FlywayMigrationTest { @Container private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withDatabaseName("migration_test") .withUsername("test") .withPassword("test"); @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", postgres::getJdbcUrl); registry.add("spring.datasource.username", postgres::getUsername); registry.add("spring.datasource.password", postgres::getPassword); registry.add("spring.flyway.locations", () -> "classpath:db/migration"); } @Autowired private DataSource dataSource; @Test void testMigrationsAppliedSuccessfully() throws Exception { // Flyway migrations run automatically on startup try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery( "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")) { List<String> tables = new ArrayList<>(); while (rs.next()) { tables.add(rs.getString(1)); } assertThat(tables).contains("users", "orders", "products"); } } @Test void testDataAfterMigration() { // Test that initial data migrations worked correctly } }

Advanced Testcontainers Patterns

Reusable Containers

@Testcontainers public class ReusableContainerTest { @Container private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withDatabaseName("testdb") .withUsername("test") .withPassword("test") .withReuse(true); // Enable container reuse @Test void testOne() { // Uses the same container instance assertThat(postgres.isRunning()).isTrue(); } @Test void testTwo() { // Reuses the same container assertThat(postgres.isRunning()).isTrue(); } } // .testcontainers.properties for global reuse // Create this file in user home directory testcontainers.reuse.enable=true

Container Lifecycle Management

public class ManualContainerManagementTest { private PostgreSQLContainer<?> postgres; @BeforeEach void setUp() { postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withDatabaseName("testdb"); postgres.start(); // Configure your application with container details System.setProperty("spring.datasource.url", postgres.getJdbcUrl()); System.setProperty("spring.datasource.username", postgres.getUsername()); System.setProperty("spring.datasource.password", postgres.getPassword()); } @AfterEach void tearDown() { if (postgres != null) { postgres.stop(); } } @Test void testWithManualManagement() { assertThat(postgres.isRunning()).isTrue(); // Your test logic here } }

Custom Container Images

public class CustomContainerTest { @Container private static final GenericContainer<?> customApp = new GenericContainer<>( DockerImageName.parse("my-registry/my-custom-app:latest") .asCompatibleSubstituteFor("nginx") ) .withExposedPorts(8080) .waitingFor(Wait.forHttp("/health").forStatusCode(200)); @Test void testCustomApplication() { String baseUrl = "http://" + customApp.getHost() + ":" + customApp.getFirstMappedPort(); // Test your custom application RestTemplate restTemplate = new RestTemplate(); ResponseEntity<String> response = restTemplate.getForEntity(baseUrl + "/health", String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } } // Building custom images programmatically public class BuiltImageTest { @Container private static final GenericContainer<?> container = new GenericContainer<>( new ImageFromDockerfile() .withDockerfileFromBuilder(builder -> builder .from("openjdk:17-jre-slim") .copy("app.jar", "/app.jar") .entryPoint("java", "-jar", "/app.jar") .build() ) .withFileFromFile("app.jar", new File("build/libs/my-app.jar")) ) .withExposedPorts(8080); }

Performance Optimization

Parallel Test Execution

@Testcontainers @Execution(ExecutionMode.CONCURRENT) // Run tests in parallel public class ParallelContainerTest { @Container private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withReuse(true); // Important for parallel execution @Test void testOne() throws InterruptedException { Thread.sleep(1000); // Simulate long test assertThat(postgres.isRunning()).isTrue(); } @Test void testTwo() throws InterruptedException { Thread.sleep(1000); // Simulate long test assertThat(postgres.isRunning()).isTrue(); } @Test void testThree() throws InterruptedException { Thread.sleep(1000); // Simulate long test assertThat(postgres.isRunning()).isTrue(); } }

Container Caching Strategies

public class ContainerCachingTest { private static final PostgreSQLContainer<?> SHARED_POSTGRES = new PostgreSQLContainer<>("postgres:15-alpine") .withReuse(true); static { SHARED_POSTGRES.start(); } @Test void testUsingSharedContainer() { // All tests use the same container instance String jdbcUrl = SHARED_POSTGRES.getJdbcUrl(); // Test logic... } }

Testing Best Practices

Test Base Classes

public abstract class AbstractIntegrationTest { protected static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); static { postgres.start(); // One-time setup for all tests configureSystemProperties(); } private static void configureSystemProperties() { System.setProperty("spring.datasource.url", postgres.getJdbcUrl()); System.setProperty("spring.datasource.username", postgres.getUsername()); System.setProperty("spring.datasource.password", postgres.getPassword()); } @BeforeEach void setUp() { // Clean database before each test cleanupDatabase(); } private void cleanupDatabase() { // Implement database cleanup logic } } // Concrete test class public class UserServiceIT extends AbstractIntegrationTest { @Test void testUserOperations() { // Test with clean database } }

Configuration Utilities

public final class TestContainerUtils { private TestContainerUtils() { // utility class } public static PostgreSQLContainer<?> createPostgreSQLContainer() { return new PostgreSQLContainer<>("postgres:15-alpine") .withDatabaseName("testdb") .withUsername("test") .withPassword("test") .withReuse(true); } public static MongoDBContainer createMongoDBContainer() { return new MongoDBContainer("mongo:6.0") .withReuse(true); } public static KafkaContainer createKafkaContainer() { return new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0")) .withReuse(true); } public static void configureSpringProperties( DynamicPropertyRegistry registry, PostgreSQLContainer<?> postgres ) { registry.add("spring.datasource.url", postgres::getJdbcUrl); registry.add("spring.datasource.username", postgres::getUsername); registry.add("spring.datasource.password", postgres::getPassword); } }

Common Issues and Solutions

Debugging Testcontainers

@Testcontainers public class DebuggingTest { @Container private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("postgres"))); @Test void testWithLogs() { // Container logs will be visible in test output assertThat(postgres.isRunning()).isTrue(); } } // Custom wait strategy public class CustomWaitStrategyTest { @Container private static final GenericContainer<?> app = new GenericContainer<>("my-app:latest") .withExposedPorts(8080) .waitingFor(Wait.forLogMessage(".*Application started.*", 1) .withStartupTimeout(Duration.ofMinutes(2))); @Test void testAppStartup() { assertThat(app.isRunning()).isTrue(); } }

Conclusion

Testcontainers provides powerful integration testing capabilities by leveraging Docker containers. Key benefits include:

  • Real Environment Testing: Test against actual databases and services
  • Isolation: Each test gets a clean environment
  • Reproducibility: Consistent environments across all machines
  • Flexibility: Support for various databases, message brokers, and custom services

Best Practices:

  • Use reusable containers when possible to improve performance
  • Implement proper cleanup between tests
  • Use appropriate wait strategies for service readiness
  • Leverage Spring Boot's Testcontainers support for seamless integration
  • Monitor container logs for debugging

When to Use Testcontainers:

  • Database integration tests
  • Microservice integration tests
  • Message broker testing
  • Any external service dependencies

Testcontainers bridges the gap between unit tests and full end-to-end tests, providing fast, reliable integration testing that closely mirrors production environments.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper