← Kursa Dön
📄 Text · 35 min

Testing Stratejileri — Container İçinde Test

CI/CD pipeline'ımızı kuruyoruz ve otomatik build ile push'ı öğrendik. Ama bir şeyi atlamamalıyız: test. Build edip push etmek kolay — ama o image'ın gerçekten doğru çalıştığından emin misin? Bu derste Docker ortamında test stratejilerini — unit test'ten E2E teste, smoke test'ten performance teste — kapsamlı şekilde inceleyeceğiz.

Bir otomobil fabrikasını düşün. Araba üretilirken her aşamada test yapılır: motor tek başına test edilir (unit test), motor + şanzıman birlikte test edilir (integration test), tüm araba test pistinde sürülür (end-to-end test), crash testi yapılır (stress test). Hiçbir araba bu testleri geçmeden fabrikadan çıkmaz.

Docker dünyasında da aynı mantık geçerli. Container'ın içindeki kodu test etmek, container'ların birbirleriyle iletişimini test etmek, tüm sistemi bir arada test etmek — her seviyede test yapmalısın. Ve Docker, test ortamlarını oluşturmayı inanılmaz kolaylaştırıyor: "benim bilgisayarımda çalışıyordu" problemi tamamen ortadan kalkıyor, çünkü herkes aynı container'da test ediyor.

Test Piramidi — Container Dünyasında

                    ┌───────────┐
                    │   E2E     │  Az sayıda, yavaş, pahalı
                    │  Tests    │  Tüm sistem birlikte
                    ├───────────┤
                    │Integration│  Orta sayıda, orta hız
                    │  Tests    │  Servisler arası iletişim
                    ├───────────┤
                    │           │  Çok sayıda, hızlı, ucuz
                    │   Unit    │  Tek bir fonksiyon/modül
                    │  Tests    │
                    └───────────┘

Docker'da Her Seviye

Test SeviyesiDocker AracıNe Test Edilir?
UnitMulti-stage build (test stage)Tek fonksiyon, sınıf, modül
IntegrationDocker Compose + test serviceServis + veritabanı, servis + cache
E2EDocker Compose (tam stack)Tüm sistem, gerçek kullanıcı senaryoları
SmokeContainer başlatma + health checkContainer ayağa kalkıyor mu?
SecurityTrivy, Scout, SnykVulnerability, misconfiguration
Performancek6, JMeter in containerYük altında performans

Unit Test — Dockerfile İçinde

Multi-stage Dockerfile kullanarak test stage'i eklersin. Test başarısız olursa build durur — hatalı image oluşmaz.

Yaklaşım 1: Ayrı Test Stage

# Dockerfile
# === Dependencies ===
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

# === Test Stage ===
FROM deps AS test
COPY . .
RUN npm run lint
RUN npm run test -- --ci --coverage --forceExit
# Test başarısız olursa build BURADA durur ❌
# Hatalı image asla oluşmaz

# === Build Stage ===
FROM deps AS builder
COPY . .
RUN npm run build
RUN npm prune --production

# === Production Stage ===
FROM node:20-alpine AS production
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER app
EXPOSE 3000
CMD ["node", "dist/index.js"]
# Sadece test stage'ini çalıştır (hızlı feedback)
docker build --target test -t myapp:test .

# Tüm build (test + production)
docker build -t myapp:latest .
# Test stage başarılı olmazsa production stage'e geçmez

Yaklaşım 2: Test Sonuçlarını Dışarı Al

FROM node:20-alpine AS test
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

# Test sonuçlarını dosyaya yaz
RUN npm run test -- --ci --coverage \
    --coverageReporters=text --coverageReporters=cobertura \
    --reporters=default --reporters=jest-junit \
    2>&1 | tee /app/test-output.txt

# Coverage raporunu kopyalanabilir yap
RUN mkdir -p /test-results && \
    cp -r coverage/ /test-results/ && \
    cp junit.xml /test-results/
# Test sonuçlarını container'dan çıkar
docker build --target test -t myapp:test .
docker create --name test-results myapp:test
docker cp test-results:/test-results ./test-results
docker rm test-results

# CI/CD'de coverage raporunu upload et

Python Örneği

# Python Dockerfile with test
FROM python:3.12-slim AS base
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM base AS test
COPY requirements-dev.txt .
RUN pip install --no-cache-dir -r requirements-dev.txt
COPY . .
RUN python -m pytest tests/ -v --tb=short --cov=app --cov-report=term-missing
RUN python -m flake8 app/
RUN python -m mypy app/

FROM base AS production
COPY app/ ./app/
RUN adduser --disabled-password appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Integration Test — Docker Compose ile

Integration testlerde uygulaman gerçek bir veritabanı, cache veya message queue ile konuşur. Docker Compose ile bu bağımlılıkları ayağa kaldırırsın.

Test Compose Dosyası

# docker-compose.test.yml
services:
  # === Test Runner ===
  test:
    build:
      context: .
      target: test-runner         # Ayrı bir stage
    environment:
      NODE_ENV: test
      DATABASE_URL: postgres://test:test@db:5432/testdb
      REDIS_URL: redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    # Container test bitince çıkmalı
    # docker compose up --abort-on-container-exit bu yüzden önemli

  # === Test Database ===
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
      POSTGRES_DB: testdb
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U test -d testdb"]
      interval: 3s
      timeout: 3s
      retries: 10
    # Volume yok — her test run temiz veritabanı

  # === Test Redis ===
  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 3s
      timeout: 3s
      retries: 10
# Dockerfile - test-runner stage
FROM node:20-alpine AS test-runner
WORKDIR /app
COPY package*.json ./
RUN npm ci               # devDependencies dahil
COPY . .
# CMD ile test komutu (Compose'da override edilebilir)
CMD ["npm", "run", "test:integration"]
# Integration testleri çalıştır
docker compose -f docker-compose.test.yml up \
    --build \
    --abort-on-container-exit \
    --exit-code-from test

# Exit code'u yakala — CI'da kullan
TEST_EXIT_CODE=$?

# Temizlik
docker compose -f docker-compose.test.yml down -v

exit $TEST_EXIT_CODE

CI/CD'de Integration Test

# .github/workflows/test.yml
jobs:
  integration-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run integration tests
        run: |
          docker compose -f docker-compose.test.yml up \
            --build \
            --abort-on-container-exit \
            --exit-code-from test

      - name: Cleanup
        if: always()
        run: docker compose -f docker-compose.test.yml down -v

Testcontainers — Programatik Container Yönetimi

Testcontainers, test kodundan container'ları programatik olarak yönetmenizi sağlar. Her test kendi izole ortamını oluşturur.

Node.js / TypeScript Örneği

// tests/integration/user.test.ts
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { RedisContainer } from '@testcontainers/redis';
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createApp } from '../../src/app';

describe('User API Integration Tests', () => {
  let pgContainer;
  let redisContainer;
  let app;

  beforeAll(async () => {
    // PostgreSQL container başlat
    pgContainer = await new PostgreSqlContainer('postgres:16-alpine')
      .withDatabase('testdb')
      .withUsername('test')
      .withPassword('test')
      .start();

    // Redis container başlat
    redisContainer = await new RedisContainer('redis:7-alpine')
      .start();

    // Uygulamayı gerçek container'larla başlat
    app = createApp({
      databaseUrl: pgContainer.getConnectionUri(),
      redisUrl: redisContainer.getConnectionUrl(),
    });

    // Migration çalıştır
    await app.runMigrations();
  }, 60000); // 60 saniye timeout (container başlatma süresi)

  afterAll(async () => {
    await pgContainer?.stop();
    await redisContainer?.stop();
  });

  it('should create a user', async () => {
    const response = await app.request('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'Test User', email: 'test@example.com' }),
    });

    expect(response.status).toBe(201);
    const user = await response.json();
    expect(user.name).toBe('Test User');
  });

  it('should handle duplicate email', async () => {
    const response = await app.request('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'Another', email: 'test@example.com' }),
    });

    expect(response.status).toBe(409);
  });
});

Python Örneği

# tests/integration/test_user_api.py
import pytest
from testcontainers.postgres import PostgresContainer
from testcontainers.redis import RedisContainer
from app.main import create_app

@pytest.fixture(scope="module")
def postgres():
    with PostgresContainer("postgres:16-alpine") as pg:
        yield pg

@pytest.fixture(scope="module")
def redis():
    with RedisContainer("redis:7-alpine") as r:
        yield r

@pytest.fixture(scope="module")
def app(postgres, redis):
    application = create_app(
        database_url=postgres.get_connection_url(),
        redis_url=f"redis://{redis.get_container_host_ip()}:{redis.get_exposed_port(6379)}"
    )
    return application

def test_create_user(app):
    client = app.test_client()
    response = client.post("/api/users", json={
        "name": "Test User",
        "email": "test@example.com"
    })
    assert response.status_code == 201
    assert response.json["name"] == "Test User"

def test_health_check(app):
    client = app.test_client()
    response = client.get("/health")
    assert response.status_code == 200
    assert response.json["status"] == "healthy"

Java Örneği

// src/test/java/com/example/UserApiTest.java
import org.junit.jupiter.api.*;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
class UserApiTest {

    @Container
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");

    @Test
    void shouldCreateUser() {
        // postgres.getJdbcUrl() → gerçek container URL'i
        var app = new App(postgres.getJdbcUrl());
        var response = app.createUser("Test User", "test@example.com");
        assertEquals(201, response.statusCode());
    }
}

E2E Test — Tam Sistem Testi

E2E testlerde tüm sistemi bir arada test edersin — frontend, backend, veritabanı, cache, message queue hepsi ayakta.

E2E Test Compose

# docker-compose.e2e.yml
services:
  # Tüm uygulama stack'i
  web:
    build:
      context: ./frontend
      target: production
    ports:
      - "3000:80"
    depends_on:
      - api

  api:
    build:
      context: ./backend
      target: production
    environment:
      DATABASE_URL: postgres://test:test@db:5432/e2e
      REDIS_URL: redis://redis:6379
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
      POSTGRES_DB: e2e
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U test"]
      interval: 3s
      retries: 10

  redis:
    image: redis:7-alpine

  # E2E test runner — Playwright
  e2e:
    build:
      context: ./e2e
      dockerfile: Dockerfile.e2e
    environment:
      BASE_URL: http://web:3000
      API_URL: http://api:3000
    depends_on:
      - web
      - api
    volumes:
      - ./e2e/test-results:/app/test-results
# e2e/Dockerfile.e2e
FROM mcr.microsoft.com/playwright:v1.41.0-jammy
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test", "--reporter=html"]
# E2E testleri çalıştır
docker compose -f docker-compose.e2e.yml up \
    --build \
    --abort-on-container-exit \
    --exit-code-from e2e

# Test raporlarına bak
open ./e2e/test-results/index.html

Smoke Test — Container Sağlık Kontrolü

En basit test: container başlıyor mu, health endpoint çalışıyor mu?

#!/bin/bash
# scripts/smoke-test.sh

IMAGE=$1
PORT=${2:-3000}
HEALTH_PATH=${3:-/health}

echo "🔥 Smoke testing $IMAGE..."

# Container'ı başlat
CONTAINER_ID=$(docker run -d -p $PORT:$PORT $IMAGE)

# Başlamasını bekle
echo "⏳ Waiting for container to start..."
sleep 5

# Health check
MAX_RETRIES=10
RETRY=0
while [ $RETRY -lt $MAX_RETRIES ]; do
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:$PORT$HEALTH_PATH 2>/dev/null)

    if [ "$HTTP_CODE" = "200" ]; then
        echo "✅ Smoke test passed! (HTTP $HTTP_CODE)"
        docker stop $CONTAINER_ID > /dev/null
        docker rm $CONTAINER_ID > /dev/null
        exit 0
    fi

    RETRY=$((RETRY + 1))
    echo "  Retry $RETRY/$MAX_RETRIES (HTTP $HTTP_CODE)..."
    sleep 2
done

echo "❌ Smoke test FAILED after $MAX_RETRIES retries"
docker logs $CONTAINER_ID
docker stop $CONTAINER_ID > /dev/null
docker rm $CONTAINER_ID > /dev/null
exit 1
# CI/CD'de smoke test
- name: Smoke test
  run: |
    docker build -t myapp:smoke .
    ./scripts/smoke-test.sh myapp:smoke 3000 /health

Performance Test — Container'da Yük Testi

k6 ile Performance Test

// load-test/script.js
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 20 },   // Ramp up
    { duration: '1m', target: 50 },    // Sabit yük
    { duration: '30s', target: 100 },  // Pik yük
    { duration: '30s', target: 0 },    // Ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],   // %95 istek 500ms altında
    http_req_failed: ['rate<0.01'],     // %1'den az hata
  },
};

export default function () {
  const res = http.get('http://api:3000/api/users');
  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 200ms': (r) => r.timings.duration < 200,
  });
  sleep(1);
}
# docker-compose.loadtest.yml
services:
  api:
    build: .
    environment:
      DATABASE_URL: postgres://test:test@db:5432/loadtest
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
      POSTGRES_DB: loadtest
    healthcheck:
      test: ["CMD-SHELL", "pg_isready"]
      interval: 3s
      retries: 10

  k6:
    image: grafana/k6:latest
    volumes:
      - ./load-test:/scripts
    command: run /scripts/script.js
    depends_on:
      - api

Container Structure Test

Google'ın container-structure-test aracı ile image'ın yapısını test edersin: dosya varlığı, komut çıktıları, metadata kontrolü.

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

# Metadata testleri
metadataTest:
  env:
    - key: NODE_ENV
      value: production
  exposedPorts: ["3000"]
  cmd: ["node", "dist/index.js"]
  user: "app"                     # Non-root user kontrolü

# Dosya testleri
fileExistenceTests:
  - name: "dist directory exists"
    path: "/app/dist"
    shouldExist: true
  - name: "no dev dependencies"
    path: "/app/node_modules/.package-lock.json"
    shouldExist: true
  - name: "no source code in production"
    path: "/app/src"
    shouldExist: false           # Kaynak kod production image'da olmamalı
  - name: "no test files"
    path: "/app/tests"
    shouldExist: false

# Komut testleri
commandTests:
  - name: "node version"
    command: "node"
    args: ["--version"]
    expectedOutput: ["v20"]
  - name: "no curl (attack surface reduction)"
    command: "which"
    args: ["curl"]
    exitCode: 1                   # curl olmamalı
# container-structure-test kurulumu
curl -LO https://storage.googleapis.com/container-structure-test/latest/container-structure-test-linux-amd64
chmod +x container-structure-test-linux-amd64
sudo mv container-structure-test-linux-amd64 /usr/local/bin/container-structure-test

# Test çalıştır
container-structure-test test \
    --image myapp:latest \
    --config container-structure-test.yaml

CI/CD'de Tam Test Pipeline

# .github/workflows/test-pipeline.yml
name: Full Test Pipeline

on:
  pull_request:
    branches: [main]

jobs:
  # 1. Unit Test (hızlı — ilk çalışır)
  unit-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run unit tests
        run: docker build --target test -t myapp:test .

  # 2. Structure Test (image yapısı)
  structure-test:
    runs-on: ubuntu-latest
    needs: unit-test
    steps:
      - uses: actions/checkout@v4
      - name: Build production image
        run: docker build -t myapp:latest .
      - name: Structure test
        run: |
          curl -LO https://storage.googleapis.com/container-structure-test/latest/container-structure-test-linux-amd64
          chmod +x container-structure-test-linux-amd64
          ./container-structure-test-linux-amd64 test \
            --image myapp:latest \
            --config container-structure-test.yaml

  # 3. Integration Test
  integration-test:
    runs-on: ubuntu-latest
    needs: unit-test
    steps:
      - uses: actions/checkout@v4
      - name: Run integration tests
        run: |
          docker compose -f docker-compose.test.yml up \
            --build --abort-on-container-exit --exit-code-from test
      - name: Cleanup
        if: always()
        run: docker compose -f docker-compose.test.yml down -v

  # 4. Security Scan
  security-scan:
    runs-on: ubuntu-latest
    needs: unit-test
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        run: docker build -t myapp:scan .
      - uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:scan
          severity: CRITICAL,HIGH
          exit-code: 1

  # 5. Smoke Test
  smoke-test:
    runs-on: ubuntu-latest
    needs: [integration-test, security-scan, structure-test]
    steps:
      - uses: actions/checkout@v4
      - name: Build and smoke test
        run: |
          docker build -t myapp:smoke .
          chmod +x scripts/smoke-test.sh
          ./scripts/smoke-test.sh myapp:smoke 3000 /health

Best Practices

Yap:

  • Test piramidini uygula: çok unit, orta integration, az E2E

  • Dockerfile'da test stage ekle — hatalı image build edilemesin

  • Integration testlerde gerçek servisler kullan (Testcontainers veya Compose)

  • Smoke test her deploy öncesi çalıştır

  • Test ortamlarında volume kullanma — her run temiz başlasın

  • --abort-on-container-exit ve --exit-code-from ile CI exit code'u yakala

  • container-structure-test ile image yapısını doğrula

  • Test Compose dosyasını ayrı tut (docker-compose.test.yml)

Yapma:

  • Sadece unit test'le yetinme — integration test kritik

  • Test veritabanına volume bağlama — testler arası veri sızar

  • E2E testleri her commit'te çalıştırma — yavaş, PR merge'de yeterli

  • Test container'larını temizlemeden bırakma (down -v unutma)

  • Mock ile her şeyi test ettiğini sanma — gerçek servislerle de test et

  • Production image ile test yapma — test bağımlılıkları (devDependencies) dahil olmamalı

Özet

  • Test piramidi: Unit (çok, hızlı) → Integration (orta) → E2E (az, yavaş) — Docker her seviyede yardımcı

  • Multi-stage Dockerfile ile test stage'i ekle — build sırasında unit test çalışır

  • Docker Compose ile integration test: gerçek DB, cache, queue ile test et

  • Testcontainers ile programatik container yönetimi — her test izole ortamda çalışır

  • Smoke test ile container'ın ayağa kalktığını ve health endpoint'in çalıştığını doğrula

  • container-structure-test ile image yapısını kontrol et (non-root, dosya varlığı, metadata)

  • Performance test (k6) ile yük altında davranışı ölç

  • CI/CD pipeline'da testleri paralel çalıştır: unit → (integration + security + structure) → smoke