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