Test Coverage Nedir: Yüzde Kaç Yeterli Kabul Edilir?

Üretim ortamına kod gönderdiğinizde içiniz rahat mı? Çoğu sysadmin ve DevOps mühendisi bu soruyu duyduğunda hafifçe gülümser, çünkü cevap çoğunlukla “pek sayılmaz” şeklindedir. Test coverage, yani test kapsamı, bu rahatsızlığı azaltmak için var olan kavramlardan biri. Ama etrafında o kadar çok mit, yanlış anlama ve “yüzde yüz olmalı” fanatizmi dolaşıyor ki, konuyu bir kez düzgünce masaya yatırmak gerekiyor.

Test Coverage Nedir, Ne Değildir

Test coverage, bir yazılımın kaynak kodunun ne kadarının testler tarafından çalıştırıldığını ölçen bir metriktir. Basitçe söylemek gerekirse: testleriniz koştuğunda kodunuzun hangi satırları, hangi dallar, hangi fonksiyonlar çalışıyor?

Burada kritik bir ayrımı baştan yapmak gerekiyor. Test coverage, kodunuzun doğru çalıştığını kanıtlamaz. Bir kod satırı test sırasında çalıştırılmış olabilir ama o satırın doğru sonucu üretip üretmediği ayrı bir mesele. Coverage, bir güvenlik ağıdır; var olması iyi, ama o ağın sağlam olup olmadığını başka şeyler belirler.

Farklı coverage türleri vardır ve bunları birbirinden ayırt etmek önemli:

  • Line Coverage (Satır Kapsamı): En basit ölçüm. Kodun kaç satırı test sırasında çalıştırıldı?
  • Branch Coverage (Dal Kapsamı): if/else, switch gibi koşullu ifadelerin her dalı test edildi mi?
  • Function Coverage (Fonksiyon Kapsamı): Tanımlı fonksiyonların kaçı en az bir kez çağrıldı?
  • Statement Coverage (İfade Kapsamı): Tek bir satırda birden fazla ifade olabilir, bunların hepsi çalıştı mı?
  • Path Coverage (Yol Kapsamı): Mümkün olan tüm kod yolları test edildi mi? Bu teorik olarak çok masraflıdır.

Gündelik sysadmin ve DevOps dünyasında genellikle line ve branch coverage ile çalışırız.

Araçlarla Coverage Ölçmek

Soyut konuşmak yerine, gerçek araçlarla nasıl ölçüldüğüne bakalım.

Python ile Coverage.py

Python ekosisteminde coverage.py fiilen standart haline gelmiş durumda. Bir mikroservis veya otomasyon scripti yazıyorsanız bunu muhtemelen zaten kullanıyorsunuzdur.

# Kurulum
pip install coverage

# Testleri coverage ile çalıştır
coverage run -m pytest tests/

# Satır bazlı rapor
coverage report -m

# HTML raporu oluştur (çok daha okunabilir)
coverage html
open htmlcov/index.html
# .coveragerc dosyası ile konfigürasyon
cat > .coveragerc << 'EOF'
[run]
source = src/
omit =
    */migrations/*
    */tests/*
    */venv/*
    setup.py

[report]
exclude_lines =
    pragma: no cover
    def __repr__
    raise NotImplementedError
    if __name__ == .__main__.:
EOF

HTML raporu açtığınızda kırmızıyla işaretlenmiş satırları görürsünüz. Bunlar hiç test edilmemiş noktalardır. Kırmızıların nerede toplandığı size çok şey anlatır: hata yönetimi kodunuz mu, edge case’ler mi, yoksa tamamen unutulmuş modüller mi?

Go ile Built-in Coverage

Go’nun coverage aracı dile gömülü olarak gelir.

# Testleri coverage ile çalıştır
go test -cover ./...

# Detaylı profil oluştur
go test -coverprofile=coverage.out ./...

# Terminalde görüntüle
go tool cover -func=coverage.out

# Tarayıcıda görüntüle
go tool cover -html=coverage.out

Go’da özellikle branch coverage önemlidir çünkü dil hata yönetimi için if err != nil pattern’ini yoğun kullanır. Her hata dalının test edilip edilmediği kritik bir sorundur.

JavaScript/TypeScript ile Jest

Frontend veya Node.js tabanlı bir servis yönetiyorsanız Jest muhtemelen karşınıza çıkacaktır.

# Coverage ile test çalıştır
npx jest --coverage

# Threshold belirle (CI için kritik)
# package.json içinde:
# "jest": {
#   "coverageThreshold": {
#     "global": {
#       "branches": 70,
#       "functions": 80,
#       "lines": 80,
#       "statements": 80
#     }
#   }
# }

# Sadece belirli dosyalar için
npx jest --coverage --collectCoverageFrom="src/**/*.{js,ts}"

CI/CD Pipeline ile Entegrasyon

Coverage ölçümünü pipeline’a entegre etmezseniz zamanla unutulup gider. Bir GitLab CI örneği:

# .gitlab-ci.yml
test:
  stage: test
  script:
    - pip install -r requirements-test.txt
    - coverage run -m pytest tests/ -v
    - coverage report --fail-under=75
    - coverage xml -o coverage.xml
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml
  coverage: '/TOTAL.*s+(d+%)$/'

--fail-under=75 parametresi kritik. Coverage eşiğin altına düşerse build başarısız olur. Bu, coverage’ın zamanla eriyen bir şeye dönüşmesini engeller.

Yüzde Kaç Yeterli?

Bu soruyu sormayan sysadmin veya geliştirici görmedim. Cevap sinir bozucu ama dürüst: bağlama göre değişir.

Ama somut rehberlik lazım, o yüzden yaygın kabul görmüş eşikleri ele alalım.

Yüzde 80 altı: Ciddi risk bölgesi. Özellikle business logic içeren kodlarda bu seviye, üretimde sürprizlerle karşılaşacağınızı gösterir. Otomasyon scriptleri ve one-off araçlar için belki kabul edilebilir, ama servisler için değil.

Yüzde 70-80: Birçok ekibin minimum hedefi. Yeni başlayan bir proje veya teknik borcu yüksek bir legacy sistem için gerçekçi bir hedef. Bu seviye “testler var, ama boşluklar var” demektir.

Yüzde 80-90: İyi durumda bir kod tabanı. Çoğu kritik yol kapsanmış, edge case’lerin bir kısmı test edilmiş. Standart servisler ve kütüphaneler için bu aralık makul bir hedeftir.

Yüzde 90 üzeri: Güvenlik açısından kritik sistemler, finansal işlemler, sağlık yazılımları için uygundur. Ama bu seviyeye ulaşmak giderek artan bir çaba ister. Son yüzde onluk genellikle en zor ve en pahalı kısımdır.

Yüzde 100: Teorik bir ideal. Pratikte nadiren mantıklı. Her satırı test etmek için harcanan zaman, elde edilen güven artışından genellikle fazladır.

Bir gerçek dünya senaryosu paylaşayım: Ödeme işleme yapan bir servis geliştiriyordunuz. Para transferi, iade, hata rollback mantığı içeriyor. Burada yüzde 90 altına inmek kabul edilemez, çünkü bir edge case kaçırdığınızda birinin parasını kaybediyorsunuz. Aynı servisteki admin paneli için ise yüzde 70 yeterli olabilir. Kritiklik seviyesine göre farklı eşikler belirlemek çok daha sağlıklı bir yaklaşımdır.

Coverage’ın Sizi Kandırdığı Durumlar

Yüksek coverage’ın sahte bir güven hissi yaratabileceğini anlatmak gerekiyor. Şu senaryoya bakın:

# Bu fonksiyonu test ediyoruz
def calculate_discount(price, user_type):
    if user_type == "premium":
        return price * 0.8
    return price

# Bu test %100 line coverage sağlar
def test_calculate_discount():
    result = calculate_discount(100, "premium")
    # assert YOK! Sadece fonksiyon çağrıldı.
    print(result)

Bu örnekte satır coverage yüzde yüzdür. Ama test, fonksiyonun doğru çalışıp çalışmadığını kontrol etmiyor. Normal kullanıcı için indirim uygulanmaması gerektiği hiç test edilmemiş. İşte coverage yüksek ama kalite düşük.

Bir başka tuzak: generated code ve boilerplate. ORM migration dosyaları, auto-generated API client’ları, proto dosyalarından üretilen kod. Bunları coverage hesabına dahil etmek, gerçek coverage rakamınızı yanıltır. .coveragerc veya jest config’de bu dosyaları hariç tutmak şarttır.

Test edilen ama assert’siz testler kadar yaygın bir sorun da mutasyon testinin yokluğudur. Mutation testing, kodunuzu küçük hatalarla değiştirerek testlerinizin bu hataları yakalayıp yakalamadığını kontrol eder.

# Python için mutmut ile mutation testing
pip install mutmut

mutmut run --paths-to-mutate src/

# Sonuçları görüntüle
mutmut results

# Öldürülemeyen mutantları göster (testlerin yakalayamadığı hatalar)
mutmut show

Mutation score, coverage’dan çok daha anlamlı bir metriktir. Ama hesaplama maliyeti yüksektir, bu yüzden genellikle kritik modüller için kullanılır.

Gerçek Dünya: Legacy Sistem Senaryosu

Diyelim ki beş yıllık bir Python monoliti devraldınız. Test coverage yüzde 23. Ne yaparsınız?

# Önce mevcut durumu tam anlayın
coverage run -m pytest tests/ --tb=short 2>/dev/null
coverage report -m --sort=cover | head -50

# En düşük coverage'lı dosyaları bul
coverage report --format=json -o coverage_report.json
python3 -c "
import json
data = json.load(open('coverage_report.json'))
files = [(k, v['summary']['percent_covered'])
         for k, v in data['files'].items()]
files.sort(key=lambda x: x[1])
for f, pct in files[:10]:
    print(f'{pct:.1f}% - {f}')
"

Bu noktada “coverage’ı hızlıca yüzde 80’e çıkarayım” dürtüsüne kapılmak tehlikelidir. Yüzde 23’ten yüzde 80’e çıkmak için genellikle testlerin gerçek kalitesinden değil, kolay satırları kapsayan yüzeysel testlerden medet umarsınız.

Daha sağlıklı yaklaşım:

  • Kritik iş mantığını belirleyin. Hangi modüller para, veri kaybı veya güvenlik ile ilgili? Oradan başlayın.
  • Regression testlerini önceliklendirin. Daha önce production’da hata vermiş kod yolları öncelikli test hedefidir.
  • Her yeni özellikle birlikte test yazın. Sadece legacy kodu retroaktif test etmeye çalışmak tükenmişliğe yol açar.
  • Coverage’ı düşürmeyin. CI’da --fail-under ile mevcut seviyeyi kilitleyin ve her PR’da bu seviyenin düşmesini engelleyin.
# Mevcut coverage'ı taban olarak kaydet ve düşmesini engelle
# Makefile örneği
test-coverage:
	coverage run -m pytest tests/
	coverage report --fail-under=23
	@echo "Coverage check passed"

# Her sprint sonunda eşiği manuel olarak artır
# --fail-under=25, sonra 30, sonra 35...

Bu ratchet yaklaşımı, hiçbir zaman geriye gitmeyi engeller ve ilerlemeyi sistematik kılar.

Farklı Kod Türleri için Farklı Stratejiler

Tek tip bir coverage hedefi belirlemek yerine, kod tabanını katmanlara ayırmak daha mantıklı:

Kritik iş mantığı ve domain servisleri: Burada yüzde 85-90 hedefleyin. Para, veri bütünlüğü, güvenlik ile ilgili her şey bu kategoride.

API katmanı ve controller’lar: Yüzde 70-80 genellikle yeterli. Integration testleri bu katmanı zaten kısmen kapsar.

Utility fonksiyonlar ve yardımcı araçlar: Yüzde 80-90. Bunlar her yerden çağrıldığı için küçük bir hata büyük hasara yol açabilir.

Konfigürasyon ve altyapı kodu: Değişken. Terraform modülleri veya Ansible rolleri için unit test yazmak anlamsız olabilir, bunun yerine integration veya smoke testleri tercih edilir.

CLI araçları ve script’ler: Yüzde 60-70 kabul edilebilir. Ama kritik production script’leri için bu eşiği yükseltmek gerekir.

# pytest ile farklı modüller için farklı threshold belirle
# pyproject.toml
[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "if __name__ == .__main__.:",
]

# conftest.py ile modül bazlı threshold kontrolü
# Bu bir custom pytest plugin örneği
def pytest_sessionfinish(session, exitstatus):
    import coverage
    cov = coverage.Coverage()
    cov.load()

    critical_modules = ["src/payments/", "src/auth/"]
    for module in critical_modules:
        data = cov.get_data()
        # Kritik modüller için özel kontrol
        print(f"Checking coverage for {module}")

SonarQube ve Coverage Entegrasyonu

Kurumsal ortamlarda SonarQube sıklıkla kullanılır. Coverage verilerini SonarQube’a göndermek, uzun vadeli takip için çok değerli.

# SonarScanner ile coverage analizi
# sonar-project.properties
cat > sonar-project.properties << 'EOF'
sonar.projectKey=my-service
sonar.projectName=My Service
sonar.sources=src/
sonar.tests=tests/
sonar.python.coverage.reportPaths=coverage.xml
sonar.coverage.exclusions=**/migrations/**,**/tests/**
EOF

# Coverage raporu oluştur ve SonarQube'a gönder
coverage run -m pytest tests/
coverage xml -o coverage.xml
sonar-scanner

SonarQube’un “Quality Gate” özelliği, coverage eşiğinin altına düşen kodu otomatik olarak engelleyebilir. Bu, coverage’ı organizasyonel bir politika haline getirir ve bireysel ekip kararına bırakmaz.

Sık Yapılan Hatalar

Yıllar içinde gördüğüm yaygın hataları paylaşayım:

Coverage için coverage yazmak: “80’e ulaşalım” diye anlamsız testler yazmak. Bir satırı çalıştırmak ama assert yazmamak. Bu durumu yakalamak için code review sırasında testlerin kalitesine coverage kadar dikkat edin.

Coverage’ı tek metrik olarak kullanmak: Coverage, bir araçtır. Kod kalitesinin bütünü değil. Cyclomatic complexity, code duplication, test execution time gibi metriklerle birlikte değerlendirilmeli.

Global threshold yerine modül bazlı threshold kullanmamak: Genel yüzde 80 hedefi, kritik bir modülün yüzde 40’ta kalmasına ve önemsiz bir modülün yüzde 95’te olmasına izin verebilir. Ortalama yanıltıcı olabilir.

Eski testleri silmemek: Test suite büyüdükçe yavaşlar. Artık var olmayan özelliklerin testleri, production’a çıkmayan kodlar için yazılmış testler birikerek hem süreyi hem de bakım yükünü artırır. Düzenli test temizliği şarttır.

# Yavaş testleri tespit et
pytest tests/ --durations=20

# Parametrize testlerde coverage çakışmasını kontrol et
pytest tests/ --collect-only | grep "test session starts" -A 1000 | wc -l

Sonuç

Test coverage, “ne kadar?” sorusuna evrensel bir cevabı olmayan ama rehberlik edici eşikleri olan bir metriktir. Sihirli bir sayı arıyorsanız, çoğu pratik senaryoda yüzde 80 makul bir başlangıç noktasıdır. Kritik sistemler için bu eşiği yüzde 90’a çıkarmak, düşük riskli araçlar için yüzde 70’e indirmek kabul edilebilir.

Ama asıl önemli olan, coverage’ı bir hedef değil, bir gösterge olarak görmektir. Yüzde 95 coverage ile dolu ama assert’siz testlerden oluşan bir suite, yüzde 75 coverage ile dolu ama kritik iş mantığını titizlikle test eden bir suite’ten çok daha değersizdir.

Pipeline’ınıza coverage kontrolü ekleyin, modül bazlı eşikler belirleyin, coverage’ı zamanla izleyin ve mutation testing ile gerçek test kalitesini ölçün. Coverage bir araçtır; doğru kullanıldığında production’a güvenle çıkmanızı sağlar, yanlış kullanıldığında size yanlış bir güvenlik hissi verir. Farkı bilmek, iyi bir sysadmin ve DevOps mühendisinin temel yetkinliklerinden biridir.

Bir yanıt yazın

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