Test coverage mengukur persentase kode yang dieksekusi oleh test. Angka coverage yang tinggi memberi rasa aman, tetapi 100% coverage bukan jaminan bebas bug. Chapter ini memperkenalkan JaCoCo sebagai tool coverage utama untuk JVM, membedakan line coverage dengan branch coverage yang lebih ketat, dan mendiskusikan kesalahpahaman tentang angka 100% — mengapa mengejar angka sering kali kontra-produktif dibandingkan menguji perilaku yang benar.

Line vs Branch Coverage #

MetrikYang dihitungKapan memuaskan
Line coverageBaris kode yang dieksekusi testSetiap baris terkena minimal sekali
Branch coverageCabang keputusan (if/else, when) yang diambilKedua cabang true dan false dilewati
Method coverageMethod yang dipanggilSetiap method dipanggil minimal sekali

Branch coverage lebih ketat daripada line coverage. Sebuah statement if (x > 0) return y menghasilkan 1 baris tetapi 2 cabang. Test yang memanggil dengan x = 5 menutupi baris tetapi hanya 1 dari 2 cabang. Branch coverage memaksa kita menulis test untuk kedua skenario.

JaCoCo untuk Kotlin #

JaCoCo (Java Code Coverage) menganalisis bytecode JVM dan menghasilkan laporan HTML/XML yang menampilkan baris dan cabang yang tercakup. Plugin Gradle jacoco otomatis mengaitkan instrumentasi ke task test.

kotlin Contoh: konfigurasi Gradle JaCoCo
// build.gradle.kts
plugins {
    kotlin("jvm") version "2.0.21"
    jacoco
}

jacoco {
    toolVersion = "0.8.12"
}

tasks.test {
    useJUnitPlatform()
    finalizedBy(tasks.jacocoTestReport)
}

tasks.jacocoTestReport {
    dependsOn(tasks.test)
    reports {
        xml.required.set(true)
        html.required.set(true)
    }
}

tasks.jacocoTestCoverageVerification {
    violationRules {
        rule {
            limit {
                counter = "BRANCH"
                minimum = "0.80".toBigDecimal()
            }
        }
    }
}

Untuk demonstrasi konsep coverage tanpa JaCoCo, berikut simulasi yang menghitung baris dan cabang yang dilewati secara manual:

kotlin
// test_coverage.kt
// Mendemonstrasikan konsep coverage (line & branch) dengan
// instrumentasi sederhana yang menghitung baris & cabang yang dilewati.

class Classifier {
    private var cabangTrueDilewati = 0
    private var cabangFalseDilewati = 0
    private var barisDieksekusi = 0

    // SUT dengan dua cabang if/else
    fun kategori(nilai: Int): String {
        barisDieksekusi++                  // baris 1: header
        return if (nilai >= 80) {          // baris 2: cabang 1
            cabangTrueDilewati++
            "Lulus"
        } else {
            cabangFalseDilewati++
            "Remedial"
        }
    }

    fun laporanCoverage(): String =
        "line=$barisDieksekusi branchTrue=$cabangTrueDilewati branchFalse=$cabangFalseDilewati"
}

fun main() {
    val c = Classifier()

    // Test suite 1: hanya menutupi cabang true (nilai >= 80)
    println("=== Skenario A: hanya nilai tinggi ===")
    c.kategori(85)
    c.kategori(95)
    println(c.laporanCoverage())
    println("Branch coverage: 1 dari 2 cabang (50%)")
    println()

    // Test suite 2: menutupi kedua cabang
    println("=== Skenario B: kedua cabang ===")
    val c2 = Classifier()
    c2.kategori(85)
    c2.kategori(60)
    println(c2.laporanCoverage())
    println("Branch coverage: 2 dari 2 cabang (100%)")
    println()

    // Demonstrasi mitos 100% coverage:
    // 100% line coverage tidak menjamin kode benar untuk semua input.
    println("=== Mitos 100% Coverage ===")
    class Bagi {
        fun bagi(a: Int, b: Int): Int = a / b // bug: tidak cek b==0
    }
    val b = Bagi()
    b.bagi(10, 2)   // menutupi 100% line, tetapi belum uji b=0
    println("100% line coverage tercapai, tetapi bug bagi(10,0) belum teruji")
    println()
    println("Kesimpulan: uji perilaku, bukan mengejar angka 100%.")
}

Output yang Dihasilkan:

text
=== Skenario A: hanya nilai tinggi ===
line=2 branchTrue=2 branchFalse=0
Branch coverage: 1 dari 2 cabang (50%)

=== Skenario B: kedua cabang ===
line=2 branchTrue=1 branchFalse=1
Branch coverage: 2 dari 2 cabang (100%)

=== Kesalahpahaman 100% Coverage ===
100% line coverage tercapai, tetapi bug bagi(10,0) belum teruji

Kesimpulan: uji perilaku, bukan mengejar angka 100%.

Mari kita bedah tiga bagian file di atas:

  1. Classifier.kategori(nilai): function dengan dua cabang if/else. Counter cabangTrueDilewati dan cabangFalseDilewati merekam jumlah kali tiap cabang dieksekusi. Pada JaCoCo nyata, instrumentasi bytecode otomatis menambahkan counter ini.
  2. Skenario A: hanya test dengan nilai >= 80. Output menunjukkan branchTrue=2 branchFalse=0 — cabang false belum pernah dilewati. Branch coverage hanya 50%.
  3. Skenario B: test dengan kedua cabang. Output branchTrue=1 branchFalse=1 — branch coverage 100%.
  4. Kesalahpahaman 100%: function Bagi.bagi(a, b) tidak memeriksa b == 0. Test yang memanggil bagi(10, 2) menutupi 100% baris tetapi tidak menemukan bug bagi(10, 0) yang melempar ArithmeticException. Coverage tidak menjamin kelengkapan test.

Kesalahpahaman tentang 100% Coverage #

Beberapa kesalahpahaman umum tentang coverage:

KlaimKenyataan
100% coverage = bebas bugSalah; coverage mengukur eksekusi, bukan ketepatan assertion
Coverage tinggi = test berkualitasTidak; test tanpa assertion yang berarti tetap menaikkan coverage
Coverage rendah = kode burukTidak selalu; kode generik yang dipakai banyak tempat mungkin punya coverage rendah pada level unit
Wajib 100% di seluruh projectTidak realistis; pertimbangkan kode generasi, kode error path yang jarang dijangkau

Apa yang Sebaiknya Diuji #

Fokus test pada area dengan risiko tinggi:

  • Domain logic: aturan bisnis yang kompleks, perhitungan, validasi
  • Edge case: input batas (0, negatif, maksimum, kosong, null)
  • Error path: penanganan exception, retry, fallback
  • Kontrak publik: API yang dipakai banyak pemanggil

Hindari menguji kode yang trivial (getter/setter) atau kode yang dijamin oleh kompilator (data class equals/hashCode).

Referensi Lanjutan #

  • JaCoCo coverage — dokumentasi JaCoCo untuk mengukur cakupan kode test dan menghasilkan laporan HTML/XML
  • kotlin.test stdlib API — rujukan assertion yang dipakai sebagai dasar perhitungan cakupan test

Selanjutnya #

Coverage mengukur seberapa banyak kode dieksekusi, tetapi tidak memikirkan apakah test memeriksa properti yang benar. Chapter property-based-testing akan memperkenalkan pendekatan berbeda: alih-alih menulis input satu per satu, library menghasilkan ratusan input acak untuk memverifikasi sifat (property) yang harus selalu benar.