CI/CDIntermediate
Docker for Test Environments
Use Docker to create consistent, reproducible test environments
7 min read
...
dockercontainerstestingenvironment
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 -aDockerfile 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 -vTesting 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: 5Pattern 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: testdbPattern 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=4443Advanced 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:testVolume 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 devNetwork 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 -vJenkins 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
*.log2. Health Checks
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js || exit 13. Resource Limits
services:
app:
image: my-app:test
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M4. 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.sql5. 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 &
waitCommon 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 testChallenge 3: Port Conflicts
Solution: Use dynamic port mapping
services:
db:
image: postgres:15
ports:
- "0:5432" # Random host portChallenge 4: Debugging
Solution: Run containers interactively
# Debug failing test
docker-compose run --rm app shSecurity 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 appuser2. Scan Images for Vulnerabilities
# Use Docker Scout or Trivy
docker scout cves my-app:test
trivy image my-app:test3. Use Secrets Securely
# Use Docker secrets
services:
app:
image: my-app:test
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txtConclusion
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
- Create a Dockerfile for your test environment
- Set up Docker Compose for integration tests
- Integrate Docker into your CI/CD pipeline
- Implement health checks and wait strategies
- Optimize build times with caching
Related Articles
Comments (0)
Loading comments...