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 Seviyesi | Docker Aracı | Ne Test Edilir? |
|---|---|---|
| Unit | Multi-stage build (test stage) | Tek fonksiyon, sınıf, modül |
| Integration | Docker Compose + test service | Servis + veritabanı, servis + cache |
| E2E | Docker Compose (tam stack) | Tüm sistem, gerçek kullanıcı senaryoları |
| Smoke | Container başlatma + health check | Container ayağa kalkıyor mu? |
| Security | Trivy, Scout, Snyk | Vulnerability, misconfiguration |
| Performance | k6, JMeter in container | Yü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çmezYaklaşı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 etPython Ö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_CODECI/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 -vTestcontainers — 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.htmlSmoke 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 /healthPerformance 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:
- apiContainer 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.yamlCI/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 /healthBest 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-exitve--exit-code-fromile CI exit code'u yakalacontainer-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 -vunutma)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
AI Asistan
Sorularını yanıtlamaya hazır