Book Free Call
Back to Blog

TestContainers for Integration Testing

Learn how to use TestContainers for reliable integration testing in Java and Node.js applications.

Introduction

Integration testing is crucial for ensuring your application works correctly with external dependencies like databases, message queues, and APIs. TestContainers provides a powerful solution for running real instances of these services in Docker containers during your tests.

What is TestContainers?

TestContainers is a library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.

Key benefits:

  • Real dependencies instead of mocks
  • Consistent test environment
  • Easy cleanup and isolation
  • Support for various technologies

Basic Example with PostgreSQL

Here's how to use TestContainers with a PostgreSQL database in Java:

@SpringBootTest
@Testcontainers
class UserRepositoryTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
            .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);
    }
    
    @Test
    void shouldCreateUser() {
        // Your test logic here
        User user = new User("John", "[email protected]");
        User saved = userRepository.save(user);
        
        assertThat(saved.getId()).isNotNull();
        assertThat(saved.getName()).isEqualTo("John");
    }
}

Node.js with TestContainers

TestContainers also works great with Node.js applications:

import { GenericContainer } from 'testcontainers';
import { Client } from 'pg';

describe('Database Integration', () => {
    let container;
    let client;

    beforeAll(async () => {
        container = await new GenericContainer('postgres:13')
            .withEnvironment({
                POSTGRES_DB: 'testdb',
                POSTGRES_USER: 'test',
                POSTGRES_PASSWORD: 'test'
            })
            .withExposedPorts(5432)
            .start();

        const port = container.getMappedPort(5432);
        client = new Client({
            host: 'localhost',
            port: port,
            database: 'testdb',
            user: 'test',
            password: 'test'
        });
        
        await client.connect();
    });

    afterAll(async () => {
        await client.end();
        await container.stop();
    });

    test('should insert and retrieve user', async () => {
        await client.query(`
            CREATE TABLE users (
                id SERIAL PRIMARY KEY,
                name VARCHAR(100),
                email VARCHAR(100)
            )
        `);
        
        await client.query(
            'INSERT INTO users (name, email) VALUES ($1, $2)',
            ['John', '[email protected]']
        );
        
        const result = await client.query('SELECT * FROM users WHERE name = $1', ['John']);
        expect(result.rows[0].email).toBe('[email protected]');
    });
});

Best Practices

Here are some best practices for using TestContainers effectively:

1. Use Static Containers for Speed

For faster test execution, use static containers that are shared across tests:

@Testcontainers
class IntegrationTest {
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13");
    
    // Tests share the same container instance
}

2. Initialize Data Consistently

Use database migration tools or setup scripts to ensure consistent test data:

@BeforeEach
void setUp() {
    // Reset database state
    jdbcTemplate.execute("TRUNCATE TABLE users CASCADE");
    
    // Insert test data
    jdbcTemplate.execute("INSERT INTO users (name, email) VALUES ('Test User', '[email protected]')");
}

3. Use Custom Images

Create custom Docker images with pre-configured data for complex scenarios:

@Container
static GenericContainer<?> customDb = new GenericContainer<>("my-custom-db:latest")
        .withExposedPorts(5432);

Conclusion

TestContainers revolutionizes integration testing by providing real, isolated environments for your tests. This leads to more reliable tests and faster development cycles.

Start using TestContainers in your next project to experience the benefits of true integration testing!

Happy testing!
Jed

Need Help with Testing Strategy?

Building robust test suites for your applications? Let's discuss how we can improve your testing approach and development workflow.

Book Your Free Call