JavaScript Test Coverage: Istanbul ve NYC Kullanımı

Bir Node.js projesinde test yazıyorsunuz, testler geçiyor, her şey güzel görünüyor. Ama gerçekten kodunuzun ne kadarını test ediyorsunuz? %30’unu mu, %70’ini mi, yoksa kritik bir iş mantığı bloğunu tamamen atladınız mı? İşte tam burada test coverage araçları devreye giriyor. Istanbul ve onun modern CLI arayüzü NYC, JavaScript dünyasında bu soruların cevabını vermek için en yaygın kullanılan araçlar. Bu yazıda ikisini de derinlemesine inceleyeceğiz, gerçek dünya senaryolarıyla nasıl kullanacağınızı göstereceğiz.

Istanbul ve NYC Nedir, Aralarındaki Fark Ne?

İstanbul, JavaScript kod coverage’ı ölçmek için kullanılan bir araçtır. Temelde kodunuzu “enstrümante” eder, yani her satır, her dal (branch), her fonksiyon için sayaçlar ekler ve testleriniz çalışırken hangi satırların çalıştığını takip eder. NYC ise Istanbul’un komut satırı arayüzüdür. Yani şunu düşünebilirsiniz: Istanbul motor, NYC ise direksiyon simidi.

Eski projelerde doğrudan istanbul komutunu görürsünüz. Yeni projelerde ise nyc kullanılıyor. Bugün itibarıyla aktif olarak geliştirilen ve önerilen yol NYC üzerinden gitmek. Istanbul v8 coverage sağlayıcısını da destekliyor artık, yani V8’in kendi içindeki coverage mekanizmasından yararlanabiliyorsunuz; bu da performans açısından ciddi bir fark yaratıyor.

Kurulum

Projenize NYC’yi eklemek oldukça basit:

npm install --save-dev nyc

Eğer Mocha veya Jest gibi bir test runner kullanıyorsanız, onları da coverage ile birlikte çalıştırmak için ek bir şey yapmanıza gerek yok çoğunlukla. Ama Jest kullanıcılarına şunu belirteyim: Jest kendi coverage mekanizmasına sahip, dolayısıyla Jest projelerinde NYC yerine --coverage flag’i yeterli. NYC daha çok Mocha, Tap, AVA gibi runner’larla birlikte kullanılıyor.

İlk Coverage Raporu

En basit kullanım şu şekilde:

npx nyc mocha tests/**/*.test.js

Bu komut Mocha ile testlerinizi çalıştırır ve ardından coverage özetini terminale basar. Tipik bir çıktı şöyle görünür:

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files |   78.43 |    65.21 |   82.35 |   78.43 |
 src/     |         |          |         |         |
  api.js  |   91.30 |    75.00 |  100.00 |   91.30 | 45,67
  auth.js |   65.21 |    50.00 |   70.00 |   65.21 | 23-31,89,102-110
----------|---------|----------|---------|---------|-------------------

Burada dört temel metrik var:

  • Statements (İfadeler): Kaç tane JavaScript ifadesi çalıştırıldı
  • Branches (Dallar): if/else, switch/case gibi karar noktalarının kaçı test edildi
  • Functions (Fonksiyonlar): Tanımlı fonksiyonların kaçı en az bir kez çağrıldı
  • Lines (Satırlar): Kaç kaynak kodu satırı çalıştırıldı

Deneyimlerime göre branch coverage en zor çıkan metriktir. Çünkü bir fonksiyonu çağırmak o fonksiyonun tüm dallarını test etmek anlamına gelmiyor. if (user.isAdmin) yazıp sadece admin kullanıcıyla test yaparsanız, false dalı hiç test edilmemiş olur.

package.json Konfigürasyonu

Projenizde NYC’yi düzgün yapılandırmak için package.json içine bir nyc bloğu ekleyebilirsiniz:

# package.json içine şu bloğu ekleyin:
{
  "nyc": {
    "include": ["src/**/*.js"],
    "exclude": ["src/**/*.test.js", "src/migrations/**"],
    "reporter": ["text", "html", "lcov"],
    "branches": 70,
    "lines": 80,
    "functions": 80,
    "statements": 80,
    "check-coverage": true
  }
}

Buradaki check-coverage: true ayarı kritik. Bu ayar aktifken belirttiğiniz eşik değerlerin altına düşerseniz NYC çıkış kodu 1 döndürür, yani CI/CD pipeline’ınızı başarısız kılar. Bu, coverage’ın zamanla erimesini önlemenin en pratik yolu.

Ayrıca .nycrc veya .nycrc.json dosyası da kullanabilirsiniz, özellikle konfigürasyonu package.json dışında tutmak istiyorsanız:

# .nycrc dosyası oluşturun
cat > .nycrc << 'EOF'
{
  "all": true,
  "include": ["src/**/*.js"],
  "exclude": [
    "**/*.test.js",
    "**/*.spec.js",
    "src/config/**",
    "src/migrations/**"
  ],
  "reporter": ["text", "html", "lcov"],
  "branches": 75,
  "lines": 80,
  "functions": 80,
  "statements": 80,
  "check-coverage": true,
  "cache": true
}
EOF

"all": true ayarına dikkat edin. Bu olmadan NYC sadece testler tarafından require edilen dosyaları raporlar. Yani hiç import edilmemiş bir dosya coverage raporuna girmez ve %0 coverage’ı gizlenmiş olur. all: true ile include pattern’ına uyan tüm dosyalar rapora dahil edilir.

Reporter Tipleri ve Kullanım Senaryoları

NYC birden fazla rapor formatını destekliyor:

# Farklı reporter formatlarını test edin
npx nyc --reporter=text mocha tests/**/*.test.js      # Terminal özeti
npx nyc --reporter=html mocha tests/**/*.test.js      # Tarayıcıda görüntülenebilir HTML
npx nyc --reporter=lcov mocha tests/**/*.test.js      # SonarQube, Codecov için
npx nyc --reporter=json mocha tests/**/*.test.js      # CI araçları için JSON
npx nyc --reporter=text-summary mocha tests/**/*.test.js  # Kısa özet

Günlük geliştirme sırasında text ya da text-summary yeterli. HTML raporu özellikle hangi satırların test edilmediğini görsel olarak incelemek istediğinizde son derece faydalı; her satırı yeşil/kırmızı renklendiriyor. LCOV formatı ise Codecov, Coveralls gibi servislere ya da SonarQube entegrasyonuna ihtiyaç duyduğunuzda kullanıyorsunuz.

V8 Coverage Provider Kullanımı

Istanbul’un klasik enstrümantasyon yöntemi yerine V8’in native coverage’ını kullanmak istiyorsanız:

npx nyc --provider=v8 mocha tests/**/*.test.js

Ya da .nycrc dosyanıza ekleyin:

# .nycrc'ye provider ekleyin
cat > .nycrc << 'EOF'
{
  "provider": "v8",
  "all": true,
  "include": ["src/**/*.js"],
  "reporter": ["text", "html", "lcov"]
}
EOF

V8 provider’ın avantajı performans. Özellikle büyük projelerde enstrümantasyon overhead’i ciddi olabiliyor. V8 bu işi runtime seviyesinde yaptığı için çok daha hızlı. Dezavantajı ise bazı edge case’lerde branch coverage raporlamasının Istanbul provider’a göre farklı (bazen daha az detaylı) çıkması.

TypeScript Projelerde NYC

TypeScript kullananlar için durum biraz farklı. Kaynak map’lere ihtiyacınız var:

npm install --save-dev nyc ts-node @istanbuljs/nyc-config-typescript source-map-support
# TypeScript projesi için .nycrc
cat > .nycrc << 'EOF'
{
  "extends": "@istanbuljs/nyc-config-typescript",
  "all": true,
  "include": ["src/**/*.ts"],
  "exclude": [
    "**/*.test.ts",
    "**/*.spec.ts",
    "src/**/*.d.ts"
  ],
  "reporter": ["text", "html", "lcov"],
  "branches": 70,
  "lines": 80,
  "functions": 80,
  "statements": 80,
  "check-coverage": true
}
EOF
# package.json scripts bölümüne ekleyin
{
  "scripts": {
    "test": "mocha --require ts-node/register tests/**/*.test.ts",
    "test:coverage": "nyc npm test"
  }
}

TypeScript’te kaynak map olmadan coverage raporu transpile edilmiş JS dosyalarını gösterir, bu da pek işe yaramaz. @istanbuljs/nyc-config-typescript paketi source map desteğini otomatik aktive ediyor.

CI/CD Pipeline Entegrasyonu

GitHub Actions ile entegrasyon örneği:

# .github/workflows/test.yml
cat > .github/workflows/test.yml << 'EOF'
name: Test and Coverage

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run tests with coverage
        run: npx nyc --reporter=lcov --reporter=text npm test
      
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info
          fail_ci_if_error: true
EOF

GitLab CI için:

# .gitlab-ci.yml kısmı
test:coverage:
  stage: test
  image: node:18-alpine
  script:
    - npm ci
    - npx nyc --reporter=text --reporter=lcov npm test
  coverage: '/Liness*:s*(d+.?d*)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
    paths:
      - coverage/
    expire_in: 1 week

GitLab’da coverage regex’i önemli. coverage: alanındaki regex terminale basılan text output’tan coverage yüzdesini parse ediyor ve GitLab’ın kendi coverage görselleştirmesine besliyor.

Çoklu Süreç ve Paralel Test Ortamları

Bazı projelerde testleri paralel çalıştırırsınız, bu durumda NYC’nin coverage verilerini birleştirmesi gerekir:

# Paralel testlerde her process kendi .nyc_output'una yazar
# Sonra bunları birleştirirsiniz:
npx nyc merge .nyc_output combined-coverage.json
npx nyc report --reporter=html --temp-dir=. --report-dir=coverage

Ya da doğrudan birden fazla test suite’i coverage ile çalıştırıp ardından raporlamak istiyorsanız:

# Her test suite'i ayrı çalıştır, coverage verilerini biriktir
npx nyc --no-clean mocha tests/unit/**/*.test.js
npx nyc --no-clean mocha tests/integration/**/*.test.js

# Birleşik rapor oluştur
npx nyc report --reporter=html --reporter=text

--no-clean flag’i burada kritik. Normal şartlarda her NYC çalışmasında .nyc_output dizini temizlenir. Bu flag ile önceki coverage verilerini koruyarak üstüne ekleme yapabilirsiniz.

Gerçek Dünya Senaryosu: Düşük Coverage’ı Artırmak

Bir projede coverage’ın %45’te olduğunu fark ettiğinizi düşünelim. Bunu sistematik olarak nasıl artırırsınız?

Önce HTML raporu açın ve kırmızı alanları inceleyin. Çoğu zaman şunları görürsünüz:

  • Error handling blokları: catch bloklarının içi neredeyse hiç test edilmemiş
  • Edge case’ler: if (items.length === 0) gibi boş array kontrolleri
  • Konfigürasyon bazlı dallar: Ortam değişkenlerine göre değişen davranışlar
# Hangi dosyaların en düşük coverage'a sahip olduğunu görmek için:
npx nyc report --reporter=text | sort -t'|' -k2 -n | head -20

Gerçek bir projede coverage’ı hızlı artırmanın en etkili yolu error path’leri test etmek. Çünkü bu dallar genellikle kısa ama çok sayıda ve tamamen test edilmemiş oluyor.

Coverage Raporlarını Yorumlamak

Yüksek coverage her zaman kaliteli test anlamına gelmez. %95 statement coverage olan ama hiç assertion içermeyen testler yazılabilir, bu durumda coverage yüksek ama testler işe yaramaz. Öte yandan %70 coverage ama iş mantığının kritik noktalarını kapsayan iyi yazılmış testler çok daha değerlidir.

Branch coverage’a özellikle dikkat edin. Statement ve line coverage aldatıcı olabilir. Bir fonksiyonu çağırdınız, satırlar yeşile döndü ama if/else dallarından sadece birini test ettiniz. Branch coverage bu durumu yakalar.

# Sadece branch coverage'ı kontrol etmek için eşik koyun
{
  "nyc": {
    "branches": 65,
    "check-coverage": true,
    "include": ["src/**/*.js"]
  }
}

Özel Durumlar: Coverage’dan Dosya veya Satır Hariç Tutmak

Bazen coverage’dan belirli satırları veya blokları çıkarmak istersiniz. Otomatik oluşturulan kod, geçici debug satırları, ya da test etmek anlamlı olmayan boilerplate:

/* istanbul ignore next */
function legacyCompatibilityShim(data) {
  // Bu fonksiyon eski API uyumu için var, test edilmeyecek
  return data;
}
if (process.env.NODE_ENV === 'development') /* istanbul ignore next */ {
  console.log('Debug mode active');
}

Bu yorumları aşırı kullanmaktan kaçının. Her istanbul ignore bir borçtur. Bir süre sonra projeye bakan biri neden ignore edildiğini anlamaz, silmekten korkar, coverage yanıltıcı kalır. Eğer kullanacaksanız yanına açıklama yazın.

Sonuç

NYC ve Istanbul, JavaScript projelerinde test kalitesini görünür kılmanın en pratik yolu. Kurulumu beş dakika, CI entegrasyonu on dakika. Ama asıl değer, coverage raporunu açıp “bu auth modülünün error handling’ini hiç test etmemişiz” dediğiniz anda ortaya çıkıyor.

Birkaç pratik öneri ile bitireyim:

  • Sıfırdan başlayan projelerde eşikleri yüksek tutun, var olan projelerde mevcut coverage’ı koruyun ve yavaş yavaş artırın
  • all: true ayarını mutlaka açın, aksi halde import edilmemiş dosyalar rapora girmez
  • HTML reporter’ı düzenli açın, sadece sayılara bakmak yeterli değil
  • Branch coverage’a özel önem gösterin, statement coverage’dan daha anlamlı
  • CI’da coverage check’i zorunlu tutun ama gerçekçi eşikler koyun

Coverage bir amaç değil, araçtır. Ama doğru kullanıldığında test edilmemiş kod alanlarını ortaya çıkarmak ve geliştirme sürecinde kaliteyi sürdürmek için son derece değerli bir araç.

Bir yanıt yazın

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