google-site-verification: google61fe8ba583a51912.html
Database Testing with Testcontainers in Java: A Complete Guide

Testcontainers is a Java library that provides lightweight, throwaway instances of databases and other dependencies for integration testing. It enables you to write real integration tests against actual databases without mocking or using in-memory databases.


Why Testcontainers for Database Testing?

Advantages:

  • Real Databases: Test against actual database engines (PostgreSQL, MySQL, etc.)
  • Isolation: Each test gets a clean database instance
  • Consistency: Same environment in development, CI, and production
  • No Mocks: Real SQL, real constraints, real behavior
  • Easy Setup: Simple API for database lifecycle management
  • Docker Based: Uses Docker containers for dependency isolation

Setup and Dependencies

Maven Dependencies

<properties> <testcontainers.version>1.19.3</testcontainers.version> </properties> <dependencies> <!-- Testcontainers Core --> <dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> <version>${testcontainers.version}</version> <scope>test</scope> </dependency> <!-- JUnit 5 Integration --> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <version>${testcontainers.version}</version> <scope>test</scope> </dependency> <!-- Database Modules --> <dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <version>${testcontainers.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>mysql</artifactId> <version>${testcontainers.version}</version> <scope>test</scope> </dependency> <!-- Spring Boot Test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- Test Database Drivers --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>test</scope> </dependency> </dependencies>

Gradle Setup

dependencies { testImplementation 'org.testcontainers:testcontainers:1.19.3' testImplementation 'org.testcontainers:junit-jupiter:1.19.3' testImplementation 'org.testcontainers:postgresql:1.19.3' testImplementation 'org.testcontainers:mysql:1.19.3' testImplementation 'org.springframework.boot:spring-boot-starter-test' }

Basic Testcontainers Examples

Example 1: Simple PostgreSQL Container Test

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.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; import static org.junit.jupiter.api.Assertions.*; @Testcontainers class BasicPostgresTest { @Container private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withDatabaseName("testdb") .withUsername("testuser") .withPassword("testpass"); @Test void testDatabaseConnection() throws Exception { // Get connection details from container String jdbcUrl = postgres.getJdbcUrl(); String username = postgres.getUsername(); String password = postgres.getPassword(); try (Connection connection = DriverManager.getConnection(jdbcUrl, username, password); Statement statement = connection.createStatement()) { // Create table statement.execute("CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(100), email VARCHAR(100))"); // Insert data statement.execute("INSERT INTO users (name, email) VALUES ('John Doe', '[email protected]')"); // Query data ResultSet resultSet = statement.executeQuery("SELECT * FROM users"); // Verify results assertTrue(resultSet.next()); assertEquals("John Doe", resultSet.getString("name")); assertEquals("[email protected]", resultSet.getString("email")); } } @Test void testIsolatedDatabase() throws Exception { // This test runs in the same container but gets a clean database String jdbcUrl = postgres.getJdbcUrl(); String username = postgres.getUsername(); String password = postgres.getPassword(); try (Connection connection = DriverManager.getConnection(jdbcUrl, username, password); Statement statement = connection.createStatement()) { // This table doesn't exist from the previous test ResultSet resultSet = statement.executeQuery( "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'users'"); resultSet.next(); assertEquals(0, resultSet.getInt(1)); // Table should not exist } } }

Example 2: MySQL Container with Custom Configuration

import org.junit.jupiter.api.Test; import org.testcontainers.containers.MySQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import java.sql.Connection; import java.sql.DriverManager; import java.sql.Statement; import static org.junit.jupiter.api.Assertions.*; @Testcontainers class MySQLTest { @Container private static final MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0") .withDatabaseName("testdb") .withUsername("testuser") .withPassword("testpass") .withCommand("--default-authentication-plugin=mysql_native_password") .withUrlParam("serverTimezone", "UTC") .withUrlParam("useSSL", "false"); @Test void testMySQLFunctionality() throws Exception { String jdbcUrl = mysql.getJdbcUrl(); String username = mysql.getUsername(); String password = mysql.getPassword(); try (Connection connection = DriverManager.getConnection(jdbcUrl, username, password); Statement statement = connection.createStatement()) { // Test MySQL specific features statement.execute("CREATE TABLE products (" + "id BIGINT AUTO_INCREMENT PRIMARY KEY, " + "name VARCHAR(255) NOT NULL, " + "price DECIMAL(10,2), " + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)"); statement.execute("INSERT INTO products (name, price) VALUES ('Laptop', 999.99)"); statement.execute("INSERT INTO products (name, price) VALUES ('Mouse', 29.99)"); var resultSet = statement.executeQuery("SELECT COUNT(*) FROM products"); resultSet.next(); assertEquals(2, resultSet.getInt(1)); // Test MySQL functions resultSet = statement.executeQuery("SELECT NOW() as current_time"); resultSet.next(); assertNotNull(resultSet.getTimestamp("current_time")); } } }

Spring Boot Integration

Example 3: Spring Boot with Testcontainers

Application Configuration:

// src/main/java/com/example/config/DatabaseConfig.java @Configuration public class DatabaseConfig { @Bean @Primary @ConfigurationProperties("spring.datasource") public DataSource dataSource() { return DataSourceBuilder.create().build(); } } // src/main/java/com/example/model/User.java @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String name; @Column(unique = true, nullable = false) private String email; private LocalDateTime createdAt; // Constructors, getters, setters public User() {} public User(String name, String email) { this.name = name; this.email = email; this.createdAt = LocalDateTime.now(); } // Getters and setters... } // src/main/java/com/example/repository/UserRepository.java public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByEmail(String email); List<User> findByNameContainingIgnoreCase(String name); }

Test Configuration:

// src/test/java/com/example/config/TestContainersConfig.java @TestConfiguration public class TestContainersConfig { @Bean @ServiceConnection // Spring Boot 3.1+ public PostgreSQLContainer<?> postgreSQLContainer() { return new PostgreSQLContainer<>("postgres:15-alpine") .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); } // For Spring Boot < 3.1, use this approach: @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); postgres.start(); registry.add("spring.datasource.url", postgres::getJdbcUrl); registry.add("spring.datasource.username", postgres::getUsername); registry.add("spring.datasource.password", postgres::getPassword); } }

Integration Tests:

// src/test/java/com/example/repository/UserRepositoryTest.java import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; 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 static org.junit.jupiter.api.Assertions.*; @DataJpaTest @Testcontainers @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) class UserRepositoryTest { @Container @ServiceConnection static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine"); @Autowired private UserRepository userRepository; @Autowired private TestEntityManager entityManager; @Test void shouldSaveUser() { User user = new User("John Doe", "[email protected]"); User saved = userRepository.save(user); assertNotNull(saved.getId()); assertEquals("John Doe", saved.getName()); assertEquals("[email protected]", saved.getEmail()); // Verify in database User found = entityManager.find(User.class, saved.getId()); assertEquals(saved.getName(), found.getName()); } @Test void shouldFindUserByEmail() { User user = new User("Jane Smith", "[email protected]"); userRepository.save(user); Optional<User> found = userRepository.findByEmail("[email protected]"); assertTrue(found.isPresent()); assertEquals("Jane Smith", found.get().getName()); } @Test void shouldReturnEmptyWhenUserNotFound() { Optional<User> found = userRepository.findByEmail("[email protected]"); assertTrue(found.isEmpty()); } @Test void shouldFindUsersByNameContaining() { userRepository.save(new User("John Doe", "[email protected]")); userRepository.save(new User("John Smith", "[email protected]")); userRepository.save(new User("Jane Doe", "[email protected]")); List<User> johns = userRepository.findByNameContainingIgnoreCase("john"); assertEquals(2, johns.size()); assertTrue(johns.stream().allMatch(user -> user.getName().toLowerCase().contains("john"))); } }

Advanced Testcontainers Features

Example 4: Database Migration with Flyway/Liquibase

import org.flywaydb.core.Flyway; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; 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 static org.junit.jupiter.api.Assertions.*; @Testcontainers class DatabaseMigrationTest { @Container private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withDatabaseName("migration_test") .withUsername("test") .withPassword("test"); private Flyway flyway; @BeforeEach void setUp() { // Configure Flyway flyway = Flyway.configure() .dataSource(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()) .locations("classpath:db/migration") .load(); // Clean and migrate flyway.clean(); flyway.migrate(); } @Test void shouldApplyMigrations() throws Exception { try (Connection connection = postgres.createConnection(""); var statement = connection.createStatement()) { // Verify tables were created ResultSet tables = statement.executeQuery( "SELECT table_name FROM information_schema.tables " + "WHERE table_schema = 'public' ORDER BY table_name"); List<String> tableNames = new ArrayList<>(); while (tables.next()) { tableNames.add(tables.getString("table_name")); } assertTrue(tableNames.contains("users")); assertTrue(tableNames.contains("products")); assertTrue(tableNames.contains("orders")); // Verify data can be inserted statement.execute("INSERT INTO users (name, email) VALUES ('Test User', '[email protected]')"); ResultSet users = statement.executeQuery("SELECT COUNT(*) FROM users"); users.next(); assertEquals(1, users.getInt(1)); } } }

Example 5: Shared Container for Multiple Test Classes

// src/test/java/com/example/containers/TestDatabaseContainer.java public class TestDatabaseContainer { private static final PostgreSQLContainer<?> CONTAINER; static { CONTAINER = new PostgreSQLContainer<>("postgres:15-alpine") .withDatabaseName("testdb") .withUsername("test") .withPassword("test") .withReuse(true); // Allows container reuse between test runs CONTAINER.start(); // Register shutdown hook Runtime.getRuntime().addShutdownHook(new Thread(CONTAINER::stop)); } public static PostgreSQLContainer<?> getInstance() { return CONTAINER; } public static String getJdbcUrl() { return CONTAINER.getJdbcUrl(); } public static String getUsername() { return CONTAINER.getUsername(); } public static String getPassword() { return CONTAINER.getPassword(); } } // src/test/java/com/example/repository/ProductRepositoryTest.java @SpringBootTest @Testcontainers class ProductRepositoryTest { @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", TestDatabaseContainer::getJdbcUrl); registry.add("spring.datasource.username", TestDatabaseContainer::getUsername); registry.add("spring.datasource.password", TestDatabaseContainer::getPassword); } @Autowired private ProductRepository productRepository; @Test void shouldSaveProduct() { Product product = new Product("Laptop", "Gaming laptop", 999.99); Product saved = productRepository.save(product); assertNotNull(saved.getId()); assertEquals("Laptop", saved.getName()); } } // src/test/java/com/example/service/OrderServiceTest.java @SpringBootTest @Testcontainers class OrderServiceTest { @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", TestDatabaseContainer::getJdbcUrl); registry.add("spring.datasource.username", TestDatabaseContainer::getUsername); registry.add("spring.datasource.password", TestDatabaseContainer::getPassword); } @Autowired private OrderService orderService; @Autowired private UserRepository userRepository; @Autowired private ProductRepository productRepository; @Test void shouldCreateOrder() { User user = userRepository.save(new User("John Doe", "[email protected]")); Product product = productRepository.save(new Product("Mouse", "Wireless mouse", 29.99)); Order order = orderService.createOrder(user.getId(), List.of(new OrderItemRequest(product.getId(), 2))); assertNotNull(order.getId()); assertEquals(59.98, order.getTotalAmount().doubleValue(), 0.01); } }

Example 6: Testing Stored Procedures and Database Functions

import org.junit.jupiter.api.BeforeAll; 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.CallableStatement; import java.sql.Connection; import java.sql.DriverManager; import java.sql.Types; import static org.junit.jupiter.api.Assertions.*; @Testcontainers class StoredProcedureTest { @Container private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withDatabaseName("procedure_test") .withUsername("test") .withPassword("test"); @BeforeAll static void setUp() throws Exception { try (Connection connection = DriverManager.getConnection( postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); var statement = connection.createStatement()) { // Create test tables statement.execute(""" CREATE TABLE employees ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, salary DECIMAL(10,2) NOT NULL, department VARCHAR(50) ) """); // Create stored procedure statement.execute(""" CREATE OR REPLACE FUNCTION calculate_bonus( employee_id INTEGER, bonus_percentage DECIMAL ) RETURNS DECIMAL AS $$ DECLARE emp_salary DECIMAL; bonus_amount DECIMAL; BEGIN SELECT salary INTO emp_salary FROM employees WHERE id = employee_id; IF emp_salary IS NULL THEN RETURN 0; END IF; bonus_amount := emp_salary * (bonus_percentage / 100); RETURN bonus_amount; END; $$ LANGUAGE plpgsql; """); // Insert test data statement.execute(""" INSERT INTO employees (name, salary, department) VALUES ('John Doe', 50000.00, 'Engineering'), ('Jane Smith', 60000.00, 'Marketing'), ('Bob Johnson', 75000.00, 'Engineering') """); } } @Test void shouldCalculateBonus() throws Exception { try (Connection connection = DriverManager.getConnection( postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); CallableStatement callableStatement = connection.prepareCall( "{ ? = call calculate_bonus(?, ?) }")) { callableStatement.registerOutParameter(1, Types.DECIMAL); callableStatement.setInt(2, 1); // employee_id callableStatement.setBigDecimal(3, new java.math.BigDecimal("10.0")); // 10% bonus callableStatement.execute(); java.math.BigDecimal bonus = callableStatement.getBigDecimal(1); assertEquals(0, new java.math.BigDecimal("5000.00").compareTo(bonus)); } } @Test void shouldReturnZeroForNonexistentEmployee() throws Exception { try (Connection connection = DriverManager.getConnection( postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); CallableStatement callableStatement = connection.prepareCall( "{ ? = call calculate_bonus(?, ?) }")) { callableStatement.registerOutParameter(1, Types.DECIMAL); callableStatement.setInt(2, 999); // nonexistent employee callableStatement.setBigDecimal(3, new java.math.BigDecimal("10.0")); callableStatement.execute(); java.math.BigDecimal bonus = callableStatement.getBigDecimal(1); assertEquals(0, java.math.BigDecimal.ZERO.compareTo(bonus)); } } }

Performance Optimization

Example 7: Container Reuse and Parallel Execution

// src/test/resources/testcontainers.properties testcontainers.reuse.enable=true // src/test/java/com/example/ParallelTest.java import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; @Execution(ExecutionMode.CONCURRENT) @Testcontainers class ParallelDatabaseTest { @Container private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withDatabaseName("parallel_test") .withReuse(true); @Test void testOne() throws Exception { executeTest("test_one"); } @Test void testTwo() throws Exception { executeTest("test_two"); } @Test void testThree() throws Exception { executeTest("test_three"); } private void executeTest(String testName) throws Exception { try (Connection connection = DriverManager.getConnection( postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); var statement = connection.createStatement()) { statement.execute("CREATE TABLE " + testName + " (id SERIAL PRIMARY KEY, data VARCHAR(100))"); statement.execute("INSERT INTO " + testName + " (data) VALUES ('" + testName + "')"); var resultSet = statement.executeQuery("SELECT data FROM " + testName); resultSet.next(); assertEquals(testName, resultSet.getString("data")); } } }

Best Practices

  1. Container Reuse: Enable reuse for faster test execution
  2. Database Cleanup: Use Flyway clean or truncate tables between tests
  3. Parallel Execution: Run independent tests in parallel
  4. Resource Management: Always close connections and resources
  5. Configuration: Externalize database configuration
  6. CI Integration: Configure Testcontainers for your CI environment
  7. Network Policies: Handle network restrictions in corporate environments

Common Issues and Solutions

Docker Daemon Not Running

// Check if Docker is available class DockerCheck { public static boolean isDockerAvailable() { try { DockerClientFactory.instance().client(); return true; } catch (Exception e) { return false; } } }

Resource Cleanup

@AfterEach void cleanup() { // Clean up test data userRepository.deleteAll(); productRepository.deleteAll(); } @AfterAll static void stopContainer() { // Only if not using reusable containers // postgres.stop(); }

Conclusion

Testcontainers provides a robust solution for database testing in Java:

Key Benefits:

  • Real Database Testing: Test against actual database engines
  • Isolation: Clean database for each test suite
  • Consistency: Same environment everywhere
  • Easy Integration: Simple setup with Spring Boot and JUnit 5
  • Flexibility: Support for multiple databases and configurations

When to Use Testcontainers:

  • Integration testing with real databases
  • Testing database migrations
  • Testing stored procedures and functions
  • Complex query testing
  • Testing database-specific features
  • CI/CD pipeline testing

Testcontainers eliminates the gap between development and production databases, providing confidence that your database interactions work correctly in real environments.


Next Steps: Explore Testcontainers modules for other services like Redis, Kafka, and Elasticsearch. Implement container reuse strategies for faster test execution and integrate with your CI/CD pipeline for reliable database testing.

Leave a Reply

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


Macro Nepal Helper