Testing in Java
Integration Testing with Testcontainers
Run real, disposable Docker instances of databases and message queues during integration tests.
Interview: Highly relevant for senior developers. Focuses on lifecycle management, port mapping dynamically, and overriding Spring property sources.
Testcontainers is a Java library that supports JUnit tests by providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything that can run in a Docker container.
Core Idea
Spin up actual containerized databases (Postgres, Redis) rather than running inaccurate in-memory (H2) databases.
Why It Matters
H2 may not support dialect-specific features (e.g. JSONB columns or Window Functions) that Postgres does, masking syntax bugs.
Interview Lens
Expect design scenarios: how to share a container lifecycle across multiple test classes, and dynamic configuration injection.
Lifecycle Management
You can configure container lifecycles in two ways:
- @Container Annotation: Tied to the JUnit test lifecycle. If the container field is static, it starts once for the whole class. If non-static, a fresh container starts for every individual test (very slow).
- Singleton Pattern (Manual Start): Start the containers manually in a base abstract class. All test classes extend this base class and share the same container instances, greatly speeding up local suite execution.
Code Walkthrough
This class demonstrates using Testcontainers to run a real PostgreSQL instance and dynamic port mapping integration.
import org.junit.jupiter.api.Test; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import java.sql.*; import static org.junit.jupiter.api.Assertions.assertTrue;@Testcontainers class OrderRepositoryTest {
@Container private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withDatabaseName("testdb") .withUsername("testuser") .withPassword("testpass");
@Test void testConnectionAndQuery() throws SQLException { // Testcontainers maps host ports dynamically to avoid port conflicts String jdbcUrl = postgres.getJdbcUrl();
try (Connection conn = DriverManager.getConnection( jdbcUrl, postgres.getUsername(), postgres.getPassword())) {
Statement stmt = conn.createStatement(); stmt.execute("CREATE TABLE orders (id INT PRIMARY KEY, amount NUMERIC);"); stmt.execute("INSERT INTO orders VALUES (1, 99.99);");
ResultSet rs = stmt.executeQuery("SELECT count(*) FROM orders;"); assertTrue(rs.next()); assertEquals(1, rs.getInt(1)); } } }
Interview-Relevant Information
Q: How do you handle dynamic ports in Testcontainers?
Answer: Testcontainers exposes ports dynamically on the host system to prevent conflicts with local software services (e.g. local PostgreSQL running on 5432). You must query the container at runtime using container.getMappedPort(port) or container.getJdbcUrl() rather than hardcoding host ports.
Q: How do you clean up containers when tests complete?
Answer: If using the @Testcontainers extension, JUnit manages termination. Under the hood, Testcontainers starts a companion container called "Ryuk" (Moby Ryuk) which monitors JVM execution and aggressively terminates all container resources if the test process aborts or completes.
Quick Checklist
Do you know why Ryuk is used? Can you write a base class that initializes a database container for dynamic configuration sharing? If yes, you are prepared for integration testing.
Use Cases
Running database integration tests against dialect-specific features like JSON operators.
Testing Apache Kafka consumers and producers against an actual containerized broker instance.
Common Mistakes
Hardcoding ports (e.g., exposing 5432:5432), which leads to build failures in environments running parallel tests or local database daemons.
Forgetting to run a Docker daemon in CI/CD pipeline environments where integration tests are scheduled.