Back to Articles
CI/CDIntermediate

Docker for Test Environments

Use Docker to create consistent, reproducible test environments

7 min read
...
dockercontainerstestingenvironment
Banner for Docker for Test Environments

Introduction

Docker enables creating isolated, consistent, and reproducible test environments. This guide covers using Docker for test automation, from local development to CI/CD pipelines.

Why Docker for Testing?

Key Benefits

1. Consistency

  • Same environment across local, CI, and production
  • "Works on my machine" problem solved
  • Reproducible test results

2. Isolation

  • Tests don't interfere with each other
  • Clean state for each test run
  • No leftover test data

3. Speed

  • Fast environment setup
  • Parallel test execution
  • Efficient resource usage

4. Flexibility

  • Test against multiple database versions
  • Different Node.js/Python/Java versions
  • Various OS configurations

Docker Basics for QE

Essential Commands

# Build an image
docker build -t my-test-app:latest .
 
# Run a container
docker run -d --name test-db -p 5432:5432 postgres:15
 
# Execute commands in container
docker exec -it test-db psql -U postgres
 
# View logs
docker logs test-db
 
# Stop and remove
docker stop test-db
docker rm test-db
 
# Clean up
docker system prune -a

Dockerfile for Test Environment

# Dockerfile.test
FROM node:18-alpine
 
# Install dependencies for testing
RUN apk add --no-cache \
    chromium \
    nss \
    freetype \
    harfbuzz \
    ca-certificates \
    ttf-freefont
 
# Set working directory
WORKDIR /app
 
# Copy package files
COPY package*.json ./
 
# Install dependencies
RUN npm ci --only=production && \
    npm cache clean --force
 
# Copy application code
COPY . .
 
# Set environment variables
ENV CHROME_BIN=/usr/bin/chromium-browser \
    PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
 
# Run tests
CMD ["npm", "test"]

Docker Compose for Test Stacks

Basic Test Stack

# docker-compose.test.yml
version: '3.8'
 
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.test
    depends_on:
      - db
      - redis
    environment:
      DATABASE_URL: postgresql://postgres:password@db:5432/testdb
      REDIS_URL: redis://redis:6379
    volumes:
      - ./tests:/app/tests
      - ./coverage:/app/coverage
  
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"
    volumes:
      - ./test-data:/docker-entrypoint-initdb.d
  
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

Running Tests with Docker Compose

# Start all services and run tests
docker-compose -f docker-compose.test.yml up --abort-on-container-exit
 
# Run tests in specific service
docker-compose -f docker-compose.test.yml run app npm test
 
# Clean up
docker-compose -f docker-compose.test.yml down -v

Testing Patterns with Docker

Pattern 1: Database Testing

# docker-compose.db-tests.yml
version: '3.8'
 
services:
  test-runner:
    image: node:18
    working_dir: /app
    command: npm run test:db
    depends_on:
      postgres:
        condition: service_healthy
      mysql:
        condition: service_healthy
    volumes:
      - .:/app
    environment:
      POSTGRES_URL: postgresql://postgres:password@postgres:5432/testdb
      MYSQL_URL: mysql://root:password@mysql:3306/testdb
  
  postgres:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: password
      POSTGRES_DB: testdb
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
  
  mysql:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: testdb
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      timeout: 5s
      retries: 5

Pattern 2: API Integration Testing

# docker-compose.api-tests.yml
version: '3.8'
 
services:
  api-tests:
    build:
      context: .
      dockerfile: Dockerfile.api-tests
    depends_on:
      - api-server
      - mock-server
    environment:
      API_BASE_URL: http://api-server:3000
      MOCK_SERVER_URL: http://mock-server:8080
    command: npm run test:api
  
  api-server:
    build: ./api
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: test
      DATABASE_URL: postgresql://postgres:password@db:5432/testdb
    depends_on:
      - db
  
  mock-server:
    image: mockserver/mockserver:latest
    ports:
      - "8080:1080"
    environment:
      MOCKSERVER_INITIALIZATION_JSON_PATH: /config/expectations.json
    volumes:
      - ./mock-expectations.json:/config/expectations.json
  
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_PASSWORD: password
      POSTGRES_DB: testdb

Pattern 3: E2E Testing with Selenium Grid

# docker-compose.e2e.yml
version: '3.8'
 
services:
  test-runner:
    build:
      context: .
      dockerfile: Dockerfile.e2e
    depends_on:
      - selenium-hub
      - chrome
      - firefox
    environment:
      SELENIUM_HUB_URL: http://selenium-hub:4444
    volumes:
      - ./tests/e2e:/app/tests/e2e
      - ./reports:/app/reports
    command: npm run test:e2e
  
  selenium-hub:
    image: selenium/hub:4.15
    ports:
      - "4444:4444"
  
  chrome:
    image: selenium/node-chrome:4.15
    shm_size: 2gb
    depends_on:
      - selenium-hub
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
  
  firefox:
    image: selenium/node-firefox:4.15
    shm_size: 2gb
    depends_on:
      - selenium-hub
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443

Advanced Docker Testing Techniques

Multi-Stage Builds for Testing

# Multi-stage Dockerfile
FROM node:18 AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci
 
FROM dependencies AS development
COPY . .
CMD ["npm", "run", "dev"]
 
FROM dependencies AS test
COPY . .
RUN npm run lint
RUN npm run test
CMD ["npm", "run", "test:coverage"]
 
FROM dependencies AS production
COPY . .
RUN npm run build
CMD ["npm", "start"]
# Build and run specific stage
docker build --target test -t my-app:test .
docker run my-app:test

Volume Mounting for Live Reload

# docker-compose.dev.yml
version: '3.8'
 
services:
  app:
    build: .
    volumes:
      - .:/app
      - /app/node_modules  # Don't overwrite node_modules
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
    command: npm run dev

Network Testing

# Test network conditions
version: '3.8'
 
services:
  app:
    build: .
    networks:
      - slow-network
  
  slow-proxy:
    image: gaiaadm/pumba
    command: >
      netem --duration 5m
      --tc-image gaiadocker/iproute2
      delay --time 300 re2:app
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - slow-network
 
networks:
  slow-network:

CI/CD Integration

GitHub Actions with Docker

name: Docker Tests
 
on: [push, pull_request]
 
jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      
      - name: Build test image
        uses: docker/build-push-action@v4
        with:
          context: .
          target: test
          tags: my-app:test
          cache-from: type=gha
          cache-to: type=gha,mode=max
      
      - name: Run tests in Docker
        run: |
          docker run --rm my-app:test
      
      - name: Run integration tests
        run: |
          docker-compose -f docker-compose.test.yml up \
            --abort-on-container-exit \
            --exit-code-from test-runner
      
      - name: Cleanup
        if: always()
        run: docker-compose -f docker-compose.test.yml down -v

Jenkins Pipeline with Docker

pipeline {
    agent any
    
    stages {
        stage('Build Test Image') {
            steps {
                script {
                    docker.build('my-app:test', '--target test .')
                }
            }
        }
        
        stage('Unit Tests') {
            steps {
                script {
                    docker.image('my-app:test').inside {
                        sh 'npm run test:unit'
                    }
                }
            }
        }
        
        stage('Integration Tests') {
            steps {
                script {
                    sh 'docker-compose -f docker-compose.test.yml up --abort-on-container-exit'
                }
            }
        }
    }
    
    post {
        always {
            sh 'docker-compose -f docker-compose.test.yml down -v'
        }
    }
}

Best Practices

1. Image Optimization

# Use specific versions
FROM node:18.17-alpine  # Not just node:latest
 
# Combine RUN commands to reduce layers
RUN apk add --no-cache git && \
    npm ci && \
    npm cache clean --force
 
# Use .dockerignore
# .dockerignore
node_modules
coverage
.git
*.log

2. Health Checks

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node healthcheck.js || exit 1

3. Resource Limits

services:
  app:
    image: my-app:test
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M

4. Test Data Management

services:
  db:
    image: postgres:15
    volumes:
      - ./test-data/init.sql:/docker-entrypoint-initdb.d/01-init.sql
      - ./test-data/seed.sql:/docker-entrypoint-initdb.d/02-seed.sql

5. Parallel Testing

# Run multiple test suites in parallel
docker-compose -f docker-compose.test.yml up -d db redis
docker-compose -f docker-compose.test.yml run --rm unit-tests &
docker-compose -f docker-compose.test.yml run --rm integration-tests &
docker-compose -f docker-compose.test.yml run --rm e2e-tests &
wait

Common Challenges and Solutions

Challenge 1: Slow Build Times

Solution: Use layer caching, multi-stage builds, and BuildKit

# Enable BuildKit
export DOCKER_BUILDKIT=1
docker build .

Challenge 2: Flaky Tests

Solution: Use health checks and wait-for scripts

# wait-for-it.sh
./wait-for-it.sh db:5432 -- npm test

Challenge 3: Port Conflicts

Solution: Use dynamic port mapping

services:
  db:
    image: postgres:15
    ports:
      - "0:5432"  # Random host port

Challenge 4: Debugging

Solution: Run containers interactively

# Debug failing test
docker-compose run --rm app sh

Security Considerations

1. Don't Run as Root

FROM node:18-alpine
 
# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
 
USER appuser

2. Scan Images for Vulnerabilities

# Use Docker Scout or Trivy
docker scout cves my-app:test
trivy image my-app:test

3. Use Secrets Securely

# Use Docker secrets
services:
  app:
    image: my-app:test
    secrets:
      - db_password
 
secrets:
  db_password:
    file: ./secrets/db_password.txt

Conclusion

Docker provides powerful capabilities for creating consistent, isolated test environments. Start with basic containers and gradually adopt advanced patterns like Docker Compose and multi-stage builds.

Next Steps

  1. Create a Dockerfile for your test environment
  2. Set up Docker Compose for integration tests
  3. Integrate Docker into your CI/CD pipeline
  4. Implement health checks and wait strategies
  5. Optimize build times with caching

Comments (0)

Loading comments...