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 #
| Metrik | Yang dihitung | Kapan memuaskan |
|---|---|---|
| Line coverage | Baris kode yang dieksekusi test | Setiap baris terkena minimal sekali |
| Branch coverage | Cabang keputusan (if/else, when) yang diambil | Kedua cabang true dan false dilewati |
| Method coverage | Method yang dipanggil | Setiap 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.
// 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:
// 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:
=== 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:
Classifier.kategori(nilai): function dengan dua cabangif/else. CountercabangTrueDilewatidancabangFalseDilewatimerekam jumlah kali tiap cabang dieksekusi. Pada JaCoCo nyata, instrumentasi bytecode otomatis menambahkan counter ini.- Skenario A: hanya test dengan
nilai >= 80. Output menunjukkanbranchTrue=2 branchFalse=0— cabang false belum pernah dilewati. Branch coverage hanya 50%. - Skenario B: test dengan kedua cabang. Output
branchTrue=1 branchFalse=1— branch coverage 100%. - Kesalahpahaman 100%: function
Bagi.bagi(a, b)tidak memeriksab == 0. Test yang memanggilbagi(10, 2)menutupi 100% baris tetapi tidak menemukan bugbagi(10, 0)yang melemparArithmeticException. Coverage tidak menjamin kelengkapan test.
Kesalahpahaman tentang 100% Coverage #
Beberapa kesalahpahaman umum tentang coverage:
| Klaim | Kenyataan |
|---|---|
| 100% coverage = bebas bug | Salah; coverage mengukur eksekusi, bukan ketepatan assertion |
| Coverage tinggi = test berkualitas | Tidak; test tanpa assertion yang berarti tetap menaikkan coverage |
| Coverage rendah = kode buruk | Tidak selalu; kode generik yang dipakai banyak tempat mungkin punya coverage rendah pada level unit |
| Wajib 100% di seluruh project | Tidak 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.