Test Driven Development (TDD) Nedir ve Nasıl Uygulanır?
Yıllar önce bir projeye dahil olduğumda, ekip “biz zaten test yazıyoruz” diyordu. Merak edip dosyalara bakınca gördüm ki testler, kod yazıldıktan sonra, deploy öncesi acelesiyle, çoğu zaman da yarım yamalak eklenmiş. “Test var” demekle “test driven development yapıyoruz” demek arasındaki fark, işte tam olarak buydu. TDD, test yazma alışkanlığından çok bir tasarım felsefesidir ve bunu anlamadan uygulamak mümkün değil.
TDD Nedir, Ne Değildir?
Test Driven Development, kodu yazmadan önce test yazma pratiğidir. Ama bu tanım yetersiz kalıyor, çünkü insanlar bunu duyunca “ah, önce test sonra kod” deyip geçiyor. Oysa TDD’nin özü şu: testler, kodun nasıl davranması gerektiğini tanımlar ve kod bu tanıma uymak zorundadır.
TDD’nin değildir listesi belki daha açıklayıcı olur:
- Yüzde yüz kod coverage garantisi değildir
- “Her şeye test yaz” baskısı değildir
- Sadece unit test yazmak değildir
- Yavaşlatan bir süreç değildir (başta öyle hissettirse de)
TDD’nin temel döngüsü üç adımdan oluşur ve buna Red-Green-Refactor denir:
- Red: Önce başarısız olan bir test yaz
- Green: Bu testi geçecek minimum kodu yaz
- Refactor: Çalışan kodu temizle, iyileştir, testi geçmeye devam ettir
Bu döngü çok küçük adımlarla işler. Bir seferde devasa bir özellik yazmaya çalışmazsın. Küçük bir davranış tanımlarsın, onu geçersin, devam edersin.
Neden Sysadmin ve DevOps Camiası TDD’ye Bakmalı?
Şimdi “bu bir yazılım geliştirme konusu, bizi ne ilgilendirir” diye düşünenler olabilir. Haklı bir soru. Ama şu gerçeklerle yüzleşelim:
- Bash scriptleri artık onlarca, yüzlerce satır
- Ansible playbook’ları karmaşıklaştı
- Python ile yazılan otomasyon araçları büyüdü
- Terraform modülleri iş mantığı içeriyor
- CI/CD pipeline’ları için yazılan Go veya Python araçları production’ı etkiliyor
Bunların hepsini “script yazdım, çalışıyor” diyerek geçiştirmek artık lüks değil. Bir restart scriptinin yanlış çalışması production’ı patlatabilir. Bir backup rotasyon scriptinin bug’ı, aylar sonra backup olmadığını gösterir.
Pratik Uygulama: Python ile Başlayalım
Python, sysadmin dünyasında en yaygın kullanılan dil olduğu için buradan başlamak mantıklı. Örnek senaryo: bir log rotasyon aracı yazıyoruz.
Önce test framework’ü kuralım:
pip install pytest pytest-cov
mkdir log_rotator
cd log_rotator
touch log_rotator.py
touch test_log_rotator.py
Şimdi TDD döngüsünü adım adım uygulayalım. İlk özellik: bir log dosyasının boyutunu kontrol eden fonksiyon.
Red aşaması – önce testi yazıyoruz:
# test_log_rotator.py
import pytest
import os
from log_rotator import should_rotate
def test_dosya_boyutu_siniri_asildiysa_rotate_et():
# 100MB sınır, 150MB dosya
assert should_rotate("/var/log/app.log", max_size_mb=100, current_size_mb=150) == True
def test_dosya_boyutu_siniri_asilmadiysa_rotate_etme():
# 100MB sınır, 50MB dosya
assert should_rotate("/var/log/app.log", max_size_mb=100, current_size_mb=50) == False
def test_dosya_tam_sinirda_rotate_etme():
# Tam sınırda olan dosya rotate edilmemeli
assert should_rotate("/var/log/app.log", max_size_mb=100, current_size_mb=100) == False
Bu testleri çalıştıralım:
pytest test_log_rotator.py -v
Tabii ki hata alırız çünkü log_rotator.py içinde henüz hiçbir şey yok. Bu Red aşaması. Testi geçmek için fonksiyonu yazmamız gerekiyor.
Green aşaması – minimum kodu yazıyoruz:
# log_rotator.py
def should_rotate(log_path: str, max_size_mb: int, current_size_mb: float) -> bool:
return current_size_mb > max_size_mb
Şimdi testi tekrar çalıştır:
pytest test_log_rotator.py -v
# Üç test de geçmeli
Refactor aşaması: Şu an kod yeterince basit, refactor gerekmiyor. Ama bir sonraki özelliği ekleyelim: gerçek dosya boyutunu okuma.
# test_log_rotator.py - yeni test ekliyoruz
import tempfile
def test_gercek_dosya_boyutunu_oku():
with tempfile.NamedTemporaryFile(delete=False, suffix='.log') as f:
f.write(b'x' * 1024 * 1024) # 1MB veri yaz
temp_path = f.name
try:
boyut = get_file_size_mb(temp_path)
assert boyut == pytest.approx(1.0, abs=0.1)
finally:
os.unlink(temp_path)
Önce test başarısız olur (get_file_size_mb fonksiyonu yok), sonra fonksiyonu yazarız:
# log_rotator.py - fonksiyon ekliyoruz
def get_file_size_mb(log_path: str) -> float:
return os.path.getsize(log_path) / (1024 * 1024)
Bash Scriptleri İçin TDD: BATS Kullanımı
Python için pytest güzel ama sysadminlerin çoğu bash yazıyor. BATS (Bash Automated Testing System) tam burada devreye giriyor.
# BATS kurulumu
git clone https://github.com/bats-core/bats-core.git
cd bats-core
./install.sh /usr/local
# veya
apt-get install bats # Debian/Ubuntu
brew install bats-core # macOS
Senaryo: Disk kullanımını kontrol eden ve kritik eşiği geçince alert üreten bir script.
#!/usr/bin/env bats
# test_disk_monitor.bats
setup() {
# Her testten önce çalışır
export TEST_THRESHOLD=80
source ./disk_monitor.sh
}
@test "disk kullanimi esik altindaysa uyari uretme" {
# df komutunu mock'luyoruz
df() { echo "Filesystem Use%"; echo "/dev/sda1 70%"; }
export -f df
result=$(check_disk_usage "/" "$TEST_THRESHOLD")
[ "$result" = "OK" ]
}
@test "disk kullanimi esigi astiysa uyari uret" {
df() { echo "Filesystem Use%"; echo "/dev/sda1 85%"; }
export -f df
result=$(check_disk_usage "/" "$TEST_THRESHOLD")
[ "$result" = "CRITICAL" ]
}
@test "gecersiz threshold hatasi don" {
run check_disk_usage "/" "invalid"
[ "$status" -eq 1 ]
}
Testi çalıştırıyoruz, hata alıyoruz:
bats test_disk_monitor.bats
Ardından disk_monitor.sh scriptini yazıyoruz:
#!/bin/bash
# disk_monitor.sh
check_disk_usage() {
local mount_point="$1"
local threshold="$2"
# Threshold validasyonu
if ! [[ "$threshold" =~ ^[0-9]+$ ]]; then
echo "Hata: Threshold sayisal olmali" >&2
return 1
fi
local usage
usage=$(df "$mount_point" | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
if [ "$usage" -gt "$threshold" ]; then
echo "CRITICAL"
else
echo "OK"
fi
}
Mock ve Stub Kavramları: Gerçek Dünya Sorunları
TDD yaparken en çok takılınan nokta dış bağımlılıklardır. Database bağlantısı, API çağrısı, dosya sistemi işlemleri… Bunları test ederken gerçek kaynakları kullanmak istemeyiz. İşte mock ve stub devreye giriyor.
Senaryo: Sunucu monitoring scripti, bir REST API’ye metrik gönderiyor.
# test_metrics_sender.py
from unittest.mock import patch, MagicMock
from metrics_sender import send_metric
def test_basarili_metrik_gonderimi():
with patch('metrics_sender.requests.post') as mock_post:
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {"status": "ok"}
result = send_metric("cpu_usage", 75.5, "web-server-01")
assert result == True
mock_post.assert_called_once()
# Gönderilen verinin doğru formatta olduğunu kontrol et
call_args = mock_post.call_args
assert call_args[1]['json']['metric_name'] == "cpu_usage"
assert call_args[1]['json']['value'] == 75.5
def test_api_hatasi_durumunda_false_don():
with patch('metrics_sender.requests.post') as mock_post:
mock_post.side_effect = ConnectionError("API erişilemiyor")
result = send_metric("cpu_usage", 75.5, "web-server-01")
assert result == False
def test_api_500_hatasi_durumunda_false_don():
with patch('metrics_sender.requests.post') as mock_post:
mock_post.return_value.status_code = 500
result = send_metric("cpu_usage", 75.5, "web-server-01")
assert result == False
Bu testleri geçecek kodu yazıyoruz:
# metrics_sender.py
import requests
import logging
logger = logging.getLogger(__name__)
def send_metric(metric_name: str, value: float, host: str) -> bool:
payload = {
"metric_name": metric_name,
"value": value,
"host": host
}
try:
response = requests.post(
"https://metrics.internal/api/v1/metrics",
json=payload,
timeout=5
)
if response.status_code == 200:
return True
logger.error(f"API hata kodu dondu: {response.status_code}")
return False
except Exception as e:
logger.error(f"Metrik gonderim hatasi: {e}")
return False
CI/CD Pipeline’a TDD Entegrasyonu
TDD yazmak güzel ama bunu otomatize etmeden yarısı kalır. GitHub Actions ile basit bir pipeline:
# .github/workflows/test.yml
name: Test Suite
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Python ortamını hazırla
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Bağımlılıkları yükle
run: |
pip install pytest pytest-cov
pip install -r requirements.txt
- name: Python testlerini çalıştır
run: |
pytest --cov=. --cov-report=xml --cov-fail-under=80
- name: BATS testlerini çalıştır
run: |
sudo apt-get install -y bats
bats tests/bash/
- name: Coverage raporunu yükle
uses: codecov/codecov-action@v3
Burada --cov-fail-under=80 parametresi önemli: coverage yüzde 80’in altına düşerse pipeline başarısız sayılır. Bu sayede “test var ama hiç coverage yok” durumu otomatik olarak engellenir.
TDD’nin Önündeki Gerçek Engeller
Teorik anlatımdan sonra dürüst olmak gerekiyor. TDD uygulamak her zaman kolay değil ve bazı gerçek engeller var:
Zaman baskısı: “Bunu bugün deploy etmem lazım” denen ortamlarda TDD zor gelir. Ama şunu anlamak gerekiyor: başta yavaş hissettiren TDD, orta vadede debug ve hotfix süresini dramatik olarak kısaltır. Bir keresinde TDD uyguladığımız bir modülde üç ay boyunca tek bir production bug’ı çıkmamıştı. Diğer modüllerde ise aynı sürede onlarca ticket açılmıştı.
Mevcut kod tabanı: Yeni başlayan projede TDD uygulamak kolay. Ama beş yıllık legacy bir sisteme TDD sokmak başka mesele. Burada tavsiyem şu: yeni feature eklerken ve bug fix yaparken TDD uygula. Mevcut koda zorla test yazmaya çalışma, önce refactor gerekir ve bu ayrı bir iş.
Test edilmesi zor kod: Bazı şeyler gerçekten zor test edilir. Sistem çağrıları, donanım etkileşimleri, zamana bağlı işlemler bunların başında gelir. Dependency injection ve mock kullanımını öğrenmek bu noktada kritik.
Ekip direnci: “Bu boşa vakit harcamak” diyenler olacak. Burada sayılarla konuşmak gerekiyor. Test yazılan ve yazılmayan modüllerin bug oranlarını, fix sürelerini karşılaştır, verilerle göster.
Coverage Analizi: Ne Kadar Yeterli?
Yüzde yüz coverage hedeflemek çoğu zaman mantıklı değil. Zaman kaybı ve bazı durumlarda anlamsız testler üretir. Gerçekçi hedefler:
- Kritik iş mantığı: Yüzde 90 ve üzeri
- Utility fonksiyonları: Yüzde 80 ve üzeri
- Konfigürasyon dosyaları: Coverage aranmaz
- UI/görsel katman: Farklı test yaklaşımı gerekir
Coverage raporunu görsel olarak incelemek için:
# HTML coverage raporu üret
pytest --cov=log_rotator --cov-report=html
# Raporu aç
open htmlcov/index.html # macOS
xdg-open htmlcov/index.html # Linux
# Sadece terminal çıktısı için
pytest --cov=log_rotator --cov-report=term-missing
--cov-report=term-missing özellikle kullanışlı: hangi satırların test edilmediğini doğrudan terminalde gösterir.
Pytest Fixtures ile Test Ortamı Yönetimi
Gerçek projelerde test verisini her test için tekrar oluşturmak zahmetli ve yavaş. Fixtures bu sorunu çözer:
# conftest.py - proje genelinde kullanılabilir fixtures
import pytest
import tempfile
import os
@pytest.fixture
def temp_log_directory():
"""Geçici log dizini oluşturur, test sonrası temizler"""
with tempfile.TemporaryDirectory() as tmpdir:
# Örnek log dosyaları oluştur
for i in range(5):
log_path = os.path.join(tmpdir, f"app.log.{i}")
with open(log_path, 'w') as f:
f.write("2024-01-01 00:00:00 INFO Test log entryn" * 1000)
yield tmpdir
# TemporaryDirectory context manager otomatik temizler
@pytest.fixture
def mock_config():
return {
"max_log_size_mb": 100,
"retention_days": 30,
"compress_after_days": 7,
"alert_threshold": 80
}
# Test dosyasında kullanım
def test_eski_loglari_temizle(temp_log_directory, mock_config):
from log_rotator import cleanup_old_logs
cleaned_count = cleanup_old_logs(
temp_log_directory,
mock_config["retention_days"]
)
assert isinstance(cleaned_count, int)
assert cleaned_count >= 0
Sonuç
TDD bir din değil, bir araç. Her projeye, her ortama körü körüne uygulamak doğru değil. Ama özellikle production’ı etkileyen scriptler, otomasyon araçları ve servisler için TDD uygulamak, uzun vadede çok ciddi bir kararlılık ve güvenilirlik sağlıyor.
Başlamak için büyük adımlar atmak şart değil. Bugün yazdığın bir sonraki Python fonksiyonu için önce testi yaz. Küçük hisset. Garip hisset. Ama devam et. Birkaç hafta sonra bak, ne kadar değiştiğini göreceksin.
Sysadmin ve DevOps dünyasında kod kalitesi artık bir “yazılımcı meselesi” değil. Altyapıyı kod olarak yönetiyoruz, otomasyonu kod olarak yazıyoruz. O zaman bu kodun kalitesini ciddiye almak da bizim işimiz.
