Join our Discord Server
Tanvir Kour Tanvir Kour is a passionate technical blogger and open source enthusiast. She is a graduate in Computer Science and Engineering and has 4 years of experience in providing IT solutions. She is well-versed with Linux, Docker and Cloud-Native application. You can connect to her via Twitter https://x.com/tanvirkour

TestContainers vs Docker: A Tale of Two Containers

2 min read

In today’s fast-paced software industry, time-to-market is a critical factor. Businesses aim to deliver products quickly, gather user feedback, and iterate rapidly to stay competitive. To achieve this agility, developers need reliable tools that enable efficient development and testing workflows. Enter Docker and Testcontainers, two essential tools that complement each other in modern software development.

The Importance of Time-to-Market

Modern software systems often tackle complex problems, leveraging diverse technologies and services. Databases, message brokers, caches, and third-party APIs are integral parts of applications today. To maintain agility and reliability:

  • Businesses need solid Continuous Integration and Continuous Deployment (CI/CD) pipelines.
  • Testing—both unit and integration—is a cornerstone of these pipelines.

The Inner-Loop Workflow

The inner-loop development workflow refers to the local, iterative cycle where developers:

  1. Write and debug code.
  2. Build the application.
  3. Verify functionality.

Docker is invaluable here. It allows developers to containerize applications, ensuring consistent environments across local, testing, and production setups.


Docker in the Inner-Loop Workflow

Docker enables developers to:

  1. Containerize applications for consistent behavior across environments.
  2. Simplify dependency management, isolating services locally.
  3. Run applications in reproducible, isolated environments without polluting the host machine.

Containerizing a Node.js Application

Here’s how to use Docker to containerize a simple Node.js application and its dependencies.

Step 1: Create a Dockerfile

FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "index.js"]

Step 2: Define docker-compose.yml

services:
  app:
    build:
      context: .
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    command: npm start

Step 3: Run the Application

$ docker compose up

With this setup, developers can focus on writing code while Docker ensures consistency across environments.


Challenges of Integration Testing

While unit tests validate isolated code logic, integration testing ensures your application works with external services like databases or APIs. However, integration testing comes with challenges:

  1. Pre-Provisioned Environments:
    • Infrastructure must be pre-configured and maintained.
    • Shared environments risk data conflicts, leading to flaky tests.
  2. In-Memory Substitutes:
    • Mocked services or in-memory substitutes like H2 for Postgres often lack parity with production systems.
    • This results in bugs that surface only after deployment.

4. Testcontainers: Simplifying Integration Testing

Testcontainers is a library that uses Docker to spin up real services in containers for integration testing. This allows developers to test with production-grade dependencies.

Advantages of Testcontainers

  • Automates setup and teardown of services during tests.
  • Runs real dependencies (e.g., Postgres, Kafka) in isolated containers.
  • Eliminates the need for shared testing environments or mocked services.
  • Supports integration testing in both local and CI environments.

Testcontainers in Action

Here’s how to use Testcontainers for a Node.js + PostgreSQL application:

Test Code

const { PostgreSQLContainer } = require('testcontainers');

describe('Integration Tests', () => {
let postgresContainer;
let dbConnection;

beforeAll(async () => {
postgresContainer = await new PostgreSQLContainer()
.withDatabase('testdb')
.withUsername('testuser')
.withPassword('testpass')
.start();

dbConnection = await connectToDatabase(
postgresContainer.getHost(),
postgresContainer.getPort(),
'testdb',
'testuser',
'testpass'
);
});

afterAll(async () => {
await dbConnection.close();
await postgresContainer.stop();
});

it('should insert and fetch data correctly', async () => {
const result = await dbConnection.query('SELECT 1 + 1 AS result');
expect(result[0].result).toBe(2);
});
});


Lifecycle of Testcontainers:

  1. Before Tests: Start a Postgres container.
  2. During Tests: Run tests against the containerized database.
  3. After Tests: Automatically stop the container.

6. How Docker and Testcontainers Work Together

Docker is the backbone for running containers. Testcontainers leverages Docker to:

  • Spin up isolated, on-demand containers during tests.
  • Replace reliance on pre-provisioned infrastructure.
  • Seamlessly bridge the gap between local testing and CI pipelines.

Testcontainers make integration testing as intuitive as unit testing by embedding Docker-based environments directly in test suites.


Benefits for CI/CD Pipelines

Accelerated Feedback Loops

  • Containers are started and stopped quickly.
  • Tests run consistently in isolated environments.

Eliminating Test Pollution

  • Each pipeline gets isolated containers, avoiding data conflicts.

Local Testing Parity

  • Developers can run integration tests locally, ensuring early feedback before CI.

Testcontainers vs. Traditional Methods

AspectTraditional MethodsTestcontainers
Environment SetupPre-provisioned/shared infraAutomated, isolated containers
Data IsolationRisk of test interferenceFully isolated per test suite
Production ParityIn-memory mocks (e.g., H2)Real services in Docker containers
Feedback CycleDelayed feedback in CIEarly feedback in local testing

Real-World Impact

Companies like Netflix, DoorDash, and Spotify have adopted Testcontainers to enhance their testing workflows. With Docker’s recent acquisition of AtomicJar (makers of Testcontainers), the synergy between Docker and Testcontainers has further evolved, simplifying testing for developers worldwide.


Conclusion

Docker accelerates the inner-loop development by ensuring consistent, isolated environments for local development. Testcontainers extends this capability into the testing phase, providing real-service integration for reliable tests.

Together, Docker and Testcontainers empower developers to deliver quality software faster, bridging the gap between development and production. If you haven’t already, it’s time to explore these tools and elevate your development workflow.

Further Reading

Have Queries? Join https://launchpass.com/collabnix

Tanvir Kour Tanvir Kour is a passionate technical blogger and open source enthusiast. She is a graduate in Computer Science and Engineering and has 4 years of experience in providing IT solutions. She is well-versed with Linux, Docker and Cloud-Native application. You can connect to her via Twitter https://x.com/tanvirkour
Join our Discord Server
Index