Snapshot Testing Nedir ve Ne Zaman Kullanılır?

Üretim ortamında bir servis güncellemeniz var ve deploy sonrası “her şey çalışıyor” diyorsunuz, ama iki gün sonra bir müşteri “bu ekran böyle görünmüyordu” diyor. İşte tam bu noktada snapshot testing’in neden var olduğunu anlıyorsunuz. Snapshot testing, yazılımın belirli bir andaki çıktısını “fotoğraflayıp” saklayan ve sonraki çalışmalarda bu fotoğrafla karşılaştırma yapan bir test yaklaşımıdır. Kulağa basit geliyor, ama doğru kullandığınızda inanılmaz güçlü bir araç.

Snapshot Testing’in Mantığı

Klasik birim testlerinde “bu fonksiyon şu girdiyle şu çıktıyı üretmeli” diye explicit bir assertion yazarsınız. Snapshot testing’de ise şunu söylüyorsunuz aslında: “Bu bileşenin, bu fonksiyonun, bu API response’unun çıktısı ne olursa olsun, şu an bu şekilde görünüyor. Bundan sonra değişirse bana söyle.”

Bu yaklaşım özellikle şu senaryolarda can kurtarıcı:

  • React veya Vue bileşenlerinin render çıktısını takip etmek
  • CLI araçlarının stdout çıktısını doğrulamak
  • REST API response yapılarının değişmediğini garantilemek
  • Konfigurasyon dosyası üretecilerinin doğru çıktı verdiğini kontrol etmek
  • Serialization/deserialization mantığını test etmek

Peki ne zaman kullanmamalısınız? Eğer test ettiğiniz şey doğası gereği değişken ise (timestamp, UUID, rastgele değer) snapshot testing sizi yoracaktır. Bunun dışında snapshot’ınız çok büyükse anlamlılığını yitirir. “500 satırlık JSON snapshot değişti” bilgisi size fazla bir şey söylemez.

Basit Bir Senaryo ile Başlayalım

Diyelim ki bir Bash script’iniz var ve bu script bir rapor üretiyor. Raporun formatı bozulursa bunu otomatik olarak yakalamak istiyorsunuz.

#!/bin/bash
# generate_report.sh

generate_server_report() {
    local hostname=$(hostname)
    local uptime=$(uptime -p)
    local load=$(cat /proc/loadavg | awk '{print $1, $2, $3}')
    
    cat <<EOF
SERVER REPORT
=============
Hostname: ${hostname}
Uptime: ${uptime}
Load Average: ${load}
Generated: TIMESTAMP_PLACEHOLDER
EOF
}

generate_server_report

Şimdi buna basit bir snapshot test mekanizması yazalım:

#!/bin/bash
# snapshot_test.sh

SNAPSHOT_DIR="./snapshots"
SCRIPT_TO_TEST="./generate_report.sh"

run_snapshot_test() {
    local test_name="$1"
    local actual_output="$2"
    local snapshot_file="${SNAPSHOT_DIR}/${test_name}.snap"
    
    mkdir -p "$SNAPSHOT_DIR"
    
    if [ ! -f "$snapshot_file" ]; then
        echo "$actual_output" > "$snapshot_file"
        echo "[CREATED] Snapshot oluşturuldu: $snapshot_file"
        return 0
    fi
    
    local expected_output=$(cat "$snapshot_file")
    
    if [ "$actual_output" = "$expected_output" ]; then
        echo "[PASS] $test_name"
        return 0
    else
        echo "[FAIL] $test_name - Snapshot eşleşmiyor!"
        echo "--- Beklenen ---"
        echo "$expected_output"
        echo "--- Gerçekleşen ---"
        echo "$actual_output"
        diff <(echo "$expected_output") <(echo "$actual_output")
        return 1
    fi
}

# Test çalıştır
REPORT_OUTPUT=$(bash "$SCRIPT_TO_TEST" | sed 's/Generated:.*/Generated: TIMESTAMP_PLACEHOLDER/')
run_snapshot_test "server_report_format" "$REPORT_OUTPUT"

Bu basit örnekte timestamp gibi dinamik değerleri normalize ettiğimize dikkat edin. Bu pattern snapshot testing’in temel pratiğidir.

Jest ile Frontend Snapshot Testing

Frontend dünyasında snapshot testing denince akla ilk Jest geliyor. React projelerinde şöyle kullanılır:

// UserCard.test.js
import React from 'react';
import renderer from 'react-test-renderer';
import UserCard from './UserCard';

describe('UserCard bileşeni', () => {
    it('standart kullanıcı verisiyle doğru render edilmeli', () => {
        const mockUser = {
            id: 1,
            username: 'ahmet.yilmaz',
            role: 'admin',
            email: '[email protected]',
            isActive: true
        };
        
        const tree = renderer
            .create(<UserCard user={mockUser} showEmail={false} />)
            .toJSON();
        
        expect(tree).toMatchSnapshot();
    });
    
    it('email görünür durumdayken render edilmeli', () => {
        const mockUser = {
            id: 1,
            username: 'ahmet.yilmaz',
            role: 'viewer',
            email: '[email protected]',
            isActive: false
        };
        
        const tree = renderer
            .create(<UserCard user={mockUser} showEmail={true} />)
            .toJSON();
        
        expect(tree).toMatchSnapshot();
    });
});

İlk çalıştırmada Jest otomatik olarak __snapshots__/UserCard.test.js.snap dosyasını oluşturur. İçeriği şuna benzer:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`UserCard bileşeni standart kullanıcı verisiyle doğru render edilmeli 1`] = `
<div
  className="user-card"
>
  <span
    className="username"
  >
    ahmet.yilmaz
  </span>
  <span
    className="role role--admin"
  >
    admin
  </span>
</div>
`;

Birisi bu bileşende role--admin class’ını yanlışlıkla kaldırırsa test anında patlar. Burada güzel olan şu: kodu inceleyen herkes snapshot dosyasına bakarak bileşenin tam olarak ne üretmesi gerektiğini görebilir. Bu aynı zamanda bir dokümantasyon görevi görür.

Snapshot’ı güncellemek gerektiğinde, yani kasıtlı bir değişiklik yaptığınızda:

# Tüm snapshot'ları güncelle
jest --updateSnapshot

# Sadece belirli bir test dosyasının snapshot'ını güncelle
jest UserCard.test.js --updateSnapshot

# Kısaltma
jest -u

Python’da Snapshot Testing: syrupy Kütüphanesi

Python dünyasında snapshot testing için syrupy kütüphanesi oldukça popüler. pytest ile entegre çalışır.

pip install syrupy
# test_api_response.py
import pytest
from myapp.api import get_user_permissions, format_audit_log

def test_user_permissions_structure(snapshot):
    """
    API response yapısı değişirse bu test yakalar.
    Yeni bir permission eklendiğinde snapshot güncellenmeli.
    """
    permissions = get_user_permissions(user_id=1, role="operator")
    assert permissions == snapshot

def test_audit_log_format(snapshot):
    """
    Audit log formatı kritik. Downstream sistemler bu formata bağımlı.
    Format değişirse hem bu test hem de entegrasyon testleri patlar.
    """
    log_entry = format_audit_log(
        action="config_change",
        user="mehmet.demir",
        resource="/api/servers/prod-01",
        timestamp="2024-01-15T10:30:00Z"  # Sabit timestamp kullanıyoruz
    )
    assert log_entry == snapshot

Snapshot’ları güncellemek için:

# Snapshot oluştur veya güncelle
pytest --snapshot-update

# Sadece belirli testleri güncelle
pytest test_api_response.py --snapshot-update

syrupy’nin güzel tarafı, snapshot’ları okunabilir formatlarda sakladığı için PR review sürecinde değişikliği net olarak görebiliyorsunuz.

CLI Araçları İçin Snapshot Testing

DevOps tarafında sıkça karşılaştığım bir senaryo: kendi yazdığınız CLI araçlarının çıktısını test etmek. Diyelim ki bir Kubernetes kaynak raporlayıcı script’iniz var.

#!/bin/bash
# test_cli_output.sh

SNAPSHOT_DIR="tests/snapshots"
UPDATE_SNAPSHOTS=${UPDATE_SNAPSHOTS:-false}
FAILED_TESTS=0
PASSED_TESTS=0

assert_snapshot() {
    local test_name="$1"
    local actual="$2"
    local snapshot_file="${SNAPSHOT_DIR}/${test_name}.snap"
    
    mkdir -p "$SNAPSHOT_DIR"
    
    # Snapshot mevcut değilse oluştur
    if [ ! -f "$snapshot_file" ] || [ "$UPDATE_SNAPSHOTS" = "true" ]; then
        echo "$actual" > "$snapshot_file"
        echo "[UPDATED] $test_name"
        return 0
    fi
    
    local expected
    expected=$(cat "$snapshot_file")
    
    if [ "$actual" = "$expected" ]; then
        echo "[PASS] $test_name"
        PASSED_TESTS=$((PASSED_TESTS + 1))
    else
        echo "[FAIL] $test_name"
        echo "Fark:"
        diff <(echo "$expected") <(echo "$actual") | head -20
        FAILED_TESTS=$((FAILED_TESTS + 1))
        return 1
    fi
}

# Test: help çıktısı
assert_snapshot "mycli_help_output" "$(./mycli --help 2>&1)"

# Test: version çıktısı  
assert_snapshot "mycli_version" "$(./mycli --version 2>&1)"

# Test: JSON çıktı formatı (timestamp normalize edilmiş)
JSON_OUTPUT=$(./mycli list --format json | python3 -c "
import json, sys
data = json.load(sys.stdin)
# Dinamik alanları normalize et
for item in data.get('items', []):
    item['created_at'] = 'NORMALIZED'
    item['last_seen'] = 'NORMALIZED'
print(json.dumps(data, indent=2, sort_keys=True))
")
assert_snapshot "mycli_list_json_format" "$JSON_OUTPUT"

echo ""
echo "Sonuç: ${PASSED_TESTS} geçti, ${FAILED_TESTS} başarısız"
[ "$FAILED_TESTS" -eq 0 ] || exit 1

Bu script’i CI/CD pipeline’ınıza şöyle entegre edebilirsiniz:

# .gitlab-ci.yml veya GitHub Actions'ta
# Snapshot'ları güncelleme ihtiyacınız varsa:
UPDATE_SNAPSHOTS=true bash test_cli_output.sh

# Normal test koşusu:
bash test_cli_output.sh

Snapshot Testing’de Karşılaşılan Tuzaklar

Yıllardır bu işi yapan biri olarak söyleyeyim: snapshot testing yanlış kullanıldığında test suite’inizi kilitler.

Dinamik verileri normalize etmemek en büyük hata. Tarih, saat, UUID, process ID gibi her çalışmada değişen değerler snapshot’a girerse testleriniz sürekli başarısız olur ve bir süre sonra kimse --updateSnapshot çalıştırmadan PR merge etmeye başlar. Bu noktada testleriniz artık güvenilir değil.

Snapshot’ları körü körüne güncellemek ikinci büyük tuzak. Jest çalıştırdınız, kırmızı çıktı gördünüz, -u flagini verdiniz, yeşile döndü. Ama neden değişti? Kasıtlı mıydı bu değişiklik? PR review’da snapshot diff’e bakılmıyorsa snapshot testing’in koruma mekanizması devre dışı kalır.

Çok büyük snapshot’lar da problem. Tüm bir sayfa HTML’ini, 200 alan içeren bir JSON objesini snapshot’lamak yerine kritik alanları izole edin.

Gerçek Bir Senaryo: Konfigurasyon Dosyası Üreticisi

Bir projede Nginx konfigurasyon dosyalarını dinamik olarak üreten bir araç yazmıştık. Bu aracın ürettiği konfig yanlışsa prod servise yazılacak ve servis düşecek. Snapshot testing burada birebir uygulandı.

# test_nginx_config_generator.py
import pytest
from pathlib import Path
from config_generator import NginxConfigGenerator

@pytest.fixture
def generator():
    return NginxConfigGenerator(template_dir="templates/nginx")

def test_basic_http_vhost(snapshot, generator):
    config = generator.generate(
        server_name="app.example.com",
        backend_port=8080,
        ssl_enabled=False,
        rate_limit="100r/s"
    )
    assert config == snapshot

def test_ssl_vhost_with_upstream(snapshot, generator):
    config = generator.generate(
        server_name="secure.example.com",
        backends=["10.0.1.10:8080", "10.0.1.11:8080"],
        ssl_enabled=True,
        ssl_cert_path="/etc/ssl/certs/example.com.crt",
        health_check=True
    )
    assert config == snapshot

def test_websocket_proxy_config(snapshot, generator):
    config = generator.generate(
        server_name="ws.example.com",
        backend_port=3000,
        websocket_support=True,
        timeout=300
    )
    assert config == snapshot

Bu testler sayesinde template’de yapılan bir değişiklik anında yakalanır. Birisi yanlışlıkla proxy_pass direktifini değiştirirse, ya da yeni bir güvenlik header’ı ekleyince başka bir şeyi bozarsa, testler hemen söyler.

CI/CD Pipeline’ına Entegrasyon

Snapshot testlerini pipeline’a doğru entegre etmek önemli. Temel prensip: snapshot dosyaları Git’e commit edilmeli ve PR’larda diff olarak görünmeli.

#!/bin/bash
# ci_snapshot_check.sh
# Bu script CI ortamında çalışır

set -e

echo "Snapshot testleri çalıştırılıyor..."

# Node.js projesi için
if [ -f "package.json" ]; then
    npx jest --ci --no-update-snapshot
    # --ci flagı mevcut olmayan snapshot için fail verir
    # --no-update-snapshot ise snapshot güncellemeyi engeller
fi

# Python projesi için
if [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then
    pytest tests/ --snapshot-warn-unused -v
    # --snapshot-warn-unused: kullanılmayan snapshot'lar için uyarı
fi

echo "Tüm snapshot testleri geçti."

GitHub Actions’ta bir workflow örneği:

# .github/workflows/snapshot-tests.yml
name: Snapshot Tests

on:
  pull_request:
    branches: [main, develop]

jobs:
  snapshot-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Node.js kurulum
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: Bağımlılıkları yükle
        run: npm ci
      
      - name: Snapshot testleri çalıştır
        run: npm test -- --ci --no-update-snapshot
      
      - name: Başarısız snapshot diff'ini artifact olarak sakla
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: snapshot-diffs
          path: |
            **/__snapshots__/
            **/snapshots/

Snapshot’ları Git’te Yönetmek

Bu konuda bazı ekiplerin yanlış yaptığı şeyleri gördüm. Snapshot dosyaları .gitignore‘a eklenmemeli, tam aksine commit edilmeli. Neden?

Çünkü snapshot dosyaları test kontratının bir parçası. Bunları versiyon kontrolüne almak şu avantajları sağlar: PR’da tam olarak neyin değiştiğini görürsünüz, başka bir geliştirici branch’inizi checkout yaptığında testler çalışır, ve kod review’unda “bu değişiklik kasıtlı mı?” sorusunu sorabilirsiniz.

# .gitignore - snapshot dosyaları BURAYA EKLENMEMELI
node_modules/
dist/
.env

# Şunları eklemek doğru DEĞIL:
# **/__snapshots__/    <-- YANLIŞ
# **/snapshots/        <-- YANLIŞ

Büyük binary snapshot’larınız varsa (örneğin görsel regression testleri), bunları Git LFS’te saklamak daha mantıklı olabilir.

Görsel Snapshot Testing

Konsol çıktısı ve JSON dışında bir de görsel snapshot testing var. Bu özellikle UI bileşenlerinin piksel düzeyinde doğruluğunu test etmek için kullanılır.

# Playwright ile görsel snapshot testi
npm install @playwright/test

# playwright.config.js'te
# snapshotDir: './visual-snapshots' olarak ayarlayın
// visual.test.js
const { test, expect } = require('@playwright/test');

test('login sayfası görsel doğrulama', async ({ page }) => {
    await page.goto('http://localhost:3000/login');
    await page.waitForLoadState('networkidle');
    
    // Tüm sayfanın screenshot'ını al ve karşılaştır
    await expect(page).toHaveScreenshot('login-page.png', {
        maxDiffPixels: 50  // 50 piksele kadar fark tolere edilir
    });
});

test('hata mesajı görünümü', async ({ page }) => {
    await page.goto('http://localhost:3000/login');
    await page.fill('#username', '[email protected]');
    await page.fill('#password', 'yanlisşifre');
    await page.click('button[type="submit"]');
    
    await page.waitForSelector('.error-message');
    
    // Sadece hata mesajı bölümünü karşılaştır
    const errorEl = page.locator('.error-message');
    await expect(errorEl).toHaveScreenshot('login-error.png');
});

Görsel snapshot testleri özellikle CSS değişikliklerinin beklenmedik bileşenleri etkilip etmediğini yakalamak için çok değerli. Ama dikkat: bu testler platform bağımlı. Linux CI’da alınan screenshot, Mac’te alınan screenshot ile piksel piksel aynı olmayabilir. Bu yüzden görsel snapshot testlerini container içinde çalıştırmanız önerilir.

Sonuç

Snapshot testing, “bu çıktı öncekiyle aynı mı?” sorusuna hızlıca cevap veren pragmatik bir test yaklaşımı. Özellikle çıktı yapısı karmaşık olan, elle assertion yazmak yorucu olan ya da regression’ları yakalamak kritik olan durumlarda çok işe yarıyor.

Benim için altın kurallar şunlar: Dinamik değerleri her zaman normalize edin, snapshot’ları körü körüne güncellemeyin ve PR review sürecinde snapshot diff’lerine mutlaka bakın. Snapshot dosyalarını versiyon kontrolüne alın ve CI’da güncellemeyi engelleyin.

Snapshot testing sihirli bir değnek değil. Birim testlerin, entegrasyon testlerin yerini tutmuyor. Ama test stratejinizin doğru bir parçası olarak konumlandırırsanız, beklenmedik değişiklikleri yakalamada inanılmaz etkili. Özellikle API kontratları, konfigurasyon üreticileri ve CLI araçları için neredeyse vazgeçilmez bir araç haline geliyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir