Skip to content

Testing Docker Containers — Integration & End-to-End Testing Guide

DodaTech Updated 2026-06-24 4 min read

Testing Docker containers ensures that your containerized application runs correctly in the target environment — with the right file system, environment variables, exposed ports, and service dependencies. In this guide, you will learn how to validate container images with structure tests, run integration tests against multi-service Docker Compose stacks, and automate end-to-end tests in CI/CD pipelines. The DodaZIP backend runs a full container test suite before every deployment, catching missing dependencies and misconfigured environments early.

Learning Path

flowchart LR
  A[Docker Basics] --> B[Docker Compose]
  B --> C[Testing Containers
You are here] C --> D[Testing Kubernetes] D --> E[CI/CD Pipelines] style C fill:#f90,color:#fff

Container Structure Tests

Validate the image content and configuration before running it:

# container-structure-test.yaml
schemaVersion: "2.0.0"

fileExistenceTests:
  - name: "nginx binary"
    path: "/usr/sbin/nginx"
    shouldExist: true
    isExecutable: true

commandTests:
  - name: "nginx version"
    command: "nginx"
    args: ["-v"]
    expectedError: ["nginx version: nginx/1.25.*"]

metadataTest:
  env:
    - key: "NGINX_PORT"
      value: "80"
  exposedPorts: ["80"]
  entrypoint: ["/docker-entrypoint.sh"]

Run the test:

container-structure-test test \
  --image nginx:1.25 \
  --config container-structure-test.yaml

Expected output:

===================================
======= TEST RESULTS ==============
===================================
[PASS] nginx binary exists and is executable
[PASS] nginx version matches expected pattern
[PASS] environment variable NGINX_PORT=80
[PASS] exposed ports include 80

Integration Tests with Docker Compose

Test your service against its real dependencies:

# docker-compose.test.yml
services:
  app:
    build: .
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/testdb
      - REDIS_URL=redis://redis:6379

  db:
    image: postgres:16
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=testdb
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d testdb"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5

  test:
    build:
      context: .
      dockerfile: Dockerfile.test
    depends_on:
      app:
        condition: service_started
    environment:
      - API_URL=http://app:8080

Run with:

docker compose -f docker-compose.test.yml up --build --abort-on-container-exit

Writing Integration Tests

Test the running container via its exposed API:

import requests
import time
import os

BASE_URL = os.getenv("API_URL", "http://localhost:8080")

def wait_for_service(url, timeout=30):
    start = time.time()
    while time.time() - start < timeout:
        try:
            r = requests.get(f"{url}/health", timeout=2)
            if r.status_code == 200:
                return True
        except requests.ConnectionError:
            pass
        time.sleep(1)
    return False

def test_health_endpoint():
    assert wait_for_service(BASE_URL), "Service did not start in time"
    r = requests.get(f"{BASE_URL}/health")
    assert r.status_code == 200
    data = r.json()
    assert data["status"] == "ok"
    assert "db" in data and data["db"] == "connected"
    assert "redis" in data and data["redis"] == "connected"
    print("Health check passed")

def test_create_resource():
    r = requests.post(f"{BASE_URL}/users", json={"name": "Alice", "email": "alice@test.com"})
    assert r.status_code == 201
    data = r.json()
    assert "id" in data
    print(f"Created user with ID: {data['id']}")

def test_get_resource():
    r = requests.get(f"{BASE_URL}/users/1")
    assert r.status_code == 200
    data = r.json()
    assert data["name"] == "Alice"
    print("Get user passed")

if __name__ == "__main__":
    test_health_endpoint()
    test_create_resource()
    test_get_resource()
    print("All container integration tests passed")

Expected output:

Health check passed
Created user with ID: 1
Get user passed
All container integration tests passed

Testing Container Logs and Errors

Verify that the container logs errors correctly:

import subprocess

def test_container_logs():
    result = subprocess.run(
        ["docker", "logs", "test_app_1"],
        capture_output=True, text=True
    )
    assert "ERROR" not in result.stdout
    assert "failed" not in result.stdout.lower()
    assert "Server started" in result.stdout
    print("Container log check passed")

test_container_logs()

Testing Image Size and Layers

import subprocess, json

def test_image_size():
    result = subprocess.run(
        ["docker", "inspect", "myapp:latest", "--format", "{{json .Size}}"],
        capture_output=True, text=True
    )
    size_bytes = int(result.stdout.strip())
    size_mb = size_bytes / (1024 * 1024)
    assert size_mb < 500, f"Image too large: {size_mb:.0f}MB"
    print(f"Image size: {size_mb:.0f}MB (limit: 500MB)")

def test_layer_count():
    result = subprocess.run(
        ["docker", "history", "--no-trunc", "--format", "{{.CreatedBy}}", "myapp:latest"],
        capture_output=True, text=True
    )
    layers = result.stdout.strip().split("\n")
    assert len(layers) <= 20, f"Too many layers: {len(layers)}"
    print(f"Layer count: {len(layers)} (limit: 20)")

test_image_size()
test_layer_count()

Practice Questions

1. What is the purpose of container structure tests?

They validate the image content — files, commands, environment variables, ports — without running the container.

2. Why should you use health checks in Docker Compose test files?

Health checks ensure dependent services are ready before the test suite starts, preventing flaky failures from race conditions.

3. What does --abort-on-container-exit do in Docker Compose?

It stops all containers when any single container exits, which is essential for CI pipelines to fail fast.

4. How can you test environment variable propagation in containers?

Use structure tests to check env vars in the image, and integration tests to verify the running container reads them correctly.

Challenge: Create a Docker Compose test stack with three services: a Python Flask API, PostgreSQL database, and Redis cache. Write integration tests covering health check, CRUD operations, and cache invalidation. Run with --abort-on-container-exit.

FAQ

What is container testing?

Container testing validates that a Docker image is correctly structured, dependencies are available, and the running container interacts properly with its environment.

How is container testing different from unit testing?

Unit testing tests code in isolation. Container testing validates the entire runtime environment — OS packages, configuration files, environment variables, and service dependencies.

Do I need a separate Dockerfile for tests?

Yes, typically a Dockerfile.test that installs test dependencies and uses a test runner instead of the production entrypoint.

How do I clean up test containers in CI?

Docker Compose handles cleanup with <a href="/devops/docker-compose/">docker compose</a> down. In CI, the ephemeral runner environment ensures no leftover containers.

What's Next

Testing Kubernetes Apps — Minikube, Kind & K3s
CI/CD Testing Pipeline
Database Testing Guide

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro