DEV Community

Cover image for Testing with Docker: A Guide to Test Containers
Anil Kumar Moka for Docker

Posted on

2 1 1

Testing with Docker: A Guide to Test Containers

Ever found yourself struggling with inconsistent test environments or spending hours setting up databases for testing? Docker test containers might be the solution you've been looking for. In this post, we'll explore how to use test containers to make your testing process more reliable and efficient.

What are Test Containers?

Test containers are lightweight, throwaway instances of your test dependencies (like databases, message queues, or other services) that can be quickly spun up for testing and torn down afterward. They're particularly useful for integration testing, where you need to test your application's interaction with external services.

Why Use Test Containers?

  1. Consistency: Every developer and CI pipeline gets the exact same environment
  2. Isolation: Each test suite can have its own fresh instance of dependencies
  3. Speed: Containers start up quickly and can be parallelized
  4. Cleanup: No leftover test data or state between runs

Getting Started

Let's create a simple example using a Node.js application with PostgreSQL. We'll use the testcontainers npm package.

First, install the necessary dependencies:

npm install --save-dev testcontainers jest
Enter fullscreen mode Exit fullscreen mode

Here's our example application code:

// db.js
const { Pool } = require('pg');

class UserRepository {
  constructor(connectionString) {
    this.pool = new Pool({
      connectionString,
    });
  }

  async createUser(name, email) {
    const result = await this.pool.query(
      'INSERT INTO users(name, email) VALUES($1, $2) RETURNING id',
      [name, email]
    );
    return result.rows[0].id;
  }

  async getUserById(id) {
    const result = await this.pool.query(
      'SELECT * FROM users WHERE id = $1',
      [id]
    );
    return result.rows[0];
  }
}

module.exports = UserRepository;
Enter fullscreen mode Exit fullscreen mode

Now, let's write our test using test containers:

// db.test.js
const { GenericContainer } = require('testcontainers');
const UserRepository = require('./db');

describe('UserRepository', () => {
  let container;
  let userRepository;

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

    // Create connection string
    const connectionString = `postgresql://test:test@${container.getHost()}:${container.getMappedPort(5432)}/testdb`;

    userRepository = new UserRepository(connectionString);

    // Initialize database schema
    await userRepository.pool.query(`
      CREATE TABLE users (
        id SERIAL PRIMARY KEY,
        name VARCHAR(100),
        email VARCHAR(100)
      )
    `);
  });

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

  it('should create and retrieve a user', async () => {
    const userId = await userRepository.createUser('John Doe', 'john@example.com');
    const user = await userRepository.getUserById(userId);

    expect(user).toEqual({
      id: userId,
      name: 'John Doe',
      email: 'john@example.com'
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Best Practices

1. Container Management

Always clean up your containers after tests:

afterAll(async () => {
  await container.stop();
});
Enter fullscreen mode Exit fullscreen mode

2. Performance Optimization

  • Use container reuse when possible
  • Run containers in parallel for different test suites
  • Consider using container networking for complex scenarios

3. Configuration Management

Store container configuration in a central place:

// testConfig.js
module.exports = {
  postgres: {
    image: 'postgres:13',
    user: 'test',
    password: 'test',
    database: 'testdb'
  }
};
Enter fullscreen mode Exit fullscreen mode

Advanced Scenarios

Custom Images

Sometimes you need a specialized container. Here's how to build one:

# Dockerfile.test
FROM postgres:13

COPY ./init.sql /docker-entrypoint-initdb.d/
Enter fullscreen mode Exit fullscreen mode
const container = await new GenericContainer()
  .withBuild({
    context: __dirname,
    dockerfile: 'Dockerfile.test'
  })
  .start();
Enter fullscreen mode Exit fullscreen mode

Multiple Containers

For microservices testing:

describe('Microservice Integration', () => {
  let pgContainer;
  let redisContainer;
  let network;

  beforeAll(async () => {
    network = await new Network().start();

    pgContainer = await new GenericContainer('postgres:13')
      .withNetwork(network)
      .start();

    redisContainer = await new GenericContainer('redis:6')
      .withNetwork(network)
      .start();
  });
});
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls and Solutions

  1. Container Cleanup: Always use afterAll hooks properly
  2. Resource Management: Monitor memory usage in CI environments
  3. Network Issues: Handle connection retries for slow-starting services
  4. Data Persistence: Be careful with volume mounts in test containers

Integration with CI/CD

Here's an example GitHub Actions workflow:

name: Test
on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '14.x'
      - run: npm install
      - run: npm test
Enter fullscreen mode Exit fullscreen mode

Conclusion

Test containers provide a powerful way to manage test dependencies and ensure consistent testing environments. They're especially valuable for teams working on applications with complex infrastructure requirements.

Remember these key takeaways:

  • Use test containers for reliable, isolated testing environments
  • Always clean up containers after tests
  • Consider performance implications in CI/CD pipelines
  • Maintain good practices around configuration management

The next time you're setting up a test environment, consider if test containers could make your life easier. They might just be the testing solution you've been looking for!


Want to learn more about Docker and testing? Follow me for more articles on software development best practices!

Image of Datadog

Create and maintain end-to-end frontend tests

Learn best practices on creating frontend tests, testing on-premise apps, integrating tests into your CI/CD pipeline, and using Datadog’s testing tunnel.

Download The Guide

Top comments (0)