Setelah ratusan test hijau, pertanyaan alami berikutnya adalah: seberapa banyak kode yang benar-benar tersentuh test? Pertanyaan itu dijawab oleh test coverage — ukuran persentase kode yang dieksekusi saat suite berjalan. Coverage adalah alat diagnosis yang sangat baik untuk menemukan metode yang belum pernah diuji sama sekali, sekaligus metrik yang sangat mudah disalahgunakan bila dijadikan target yang dipuaskan dengan test tanpa makna. Pada Chapter ini kita akan membedah tiga jenis ukuran coverage, memodelkan cara kerja JaCoCo dengan instrumentasi manual dari Java murni, lalu belajar membaca laporan dan menetapkan angka sasaran yang sehat.

Tiga Jenis Ukuran Coverage #

Coverage bukan satu angka melainkan beberapa sudut pandang:

JenisMenghitungContoh celah yang terlihat
Line/statementBaris atau statement yang dieksekusiBaris return "remedial" yang tak pernah dicapai
BranchCabang keputusan yang dilalui (if benar dan salah, case)Kondisi nilai >= 60 hanya pernah bernilai false
PathKombinasi cabang dalam satu methodUrutan if pertama benar lalu if kedua salah

Branch coverage lebih ketat daripada line coverage: satu baris if dihitung tercakup meski hanya cabang true yang pernah dijalankan, padahal cabang false belum teruji. Path coverage paling ketat dan paling mahal — jumlah kombinasi tumbuh secara perkalian terhadap jumlah kondisi, sehingga mencapai path coverage penuh pada method dengan lima kondisi berarti menguji 32 jalur. Karena itu praktik industri berhenti di line dan branch coverage, lalu memakai property-based testing (Chapter tersendiri) untuk menggantikan sebagian peran path.

Instrumentasi Manual sebagai Model JaCoCo #

Bagaimana alat coverage tahu baris mana yang dieksekusi? JaCoCo menyisipkan penghitung ke bytecode saat class dimuat — teknik bernama instrumentasi. Kita bisa memodelkan teknik itu secara eksplisit: array penghitung, satu slot per cabang, dinaikkan setiap kali cabang dilalui:

public class ContohCoverage {

    // instrumentasi manual: satu penghitung per cabang
    static final int[] kunjunganCabang = new int[3];

    static String kategoriNilai(int nilai) {
        if (nilai >= 80) {
            kunjunganCabang[0]++;
            return "unggul";
        }
        if (nilai >= 60) {
            kunjunganCabang[1]++;
            return "lulus";
        }
        kunjunganCabang[2]++;
        return "remedial";
    }

    static void laporan() {
        String[] label = {
            "nilai >= 80 -> unggul",
            "nilai >= 60 -> lulus",
            "lainnya     -> remedial"
        };
        int cabangTerkunjungi = 0;
        for (int i = 0; i < kunjunganCabang.length; i++) {
            System.out.printf("cabang %-24s : %d kunjungan%n", label[i], kunjunganCabang[i]);
            if (kunjunganCabang[i] > 0) {
                cabangTerkunjungi++;
            }
        }
        System.out.println("branch coverage : "
            + cabangTerkunjungi + "/" + kunjunganCabang.length + " cabang");
    }

    public static void main(String[] args) {
        System.out.println("== suite awal: test nilai 85 dan 72 ==");
        System.out.println("hasil : " + kategoriNilai(85) + ", " + kategoriNilai(72));
        laporan();

        System.out.println("== suite ditambah test nilai 45 ==");
        System.out.println("hasil : " + kategoriNilai(45));
        laporan();
    }
}

Output yang Dihasilkan:

text
== suite awal: test nilai 85 dan 72 ==
hasil : unggul, lulus
cabang nilai >= 80 -> unggul    : 1 kunjungan
cabang nilai >= 60 -> lulus     : 1 kunjungan
cabang lainnya     -> remedial  : 0 kunjungan
branch coverage : 2/3 cabang
== suite ditambah test nilai 45 ==
hasil : remedial
cabang nilai >= 80 -> unggul    : 1 kunjungan
cabang nilai >= 60 -> lulus     : 1 kunjungan
cabang lainnya     -> remedial  : 1 kunjungan
branch coverage : 3/3 cabang

Laporan suite awal memperlihatkan celah yang justru tidak terlihat pada line coverage. Bedah baris-per-baris bagian kuncinya:

  1. Baris static final int[] kunjunganCabang = new int[3];: instrumentasi versi manual — satu slot per cabang; JaCoCo melakukan hal serupa lewat array penghitung yang disisipkan ke bytecode.
  2. Baris kunjunganCabang[0]++;: penambahan penghitung ditempatkan persis di dalam cabang yang diwakilinya; setiap eksekusi cabang meninggalkan jejak permanen pada array.
  3. Baris if (kunjunganCabang[i] > 0) cabangTerkunjungi++;: laporan menghitung cabang yang kunjungannya lebih dari nol — definisi branch coverage dalam satu baris.
  4. Baris kategoriNilai(85) dan kategoriNilai(72): dua test pertama mengeksekusi hampir seluruh baris method (line coverage tinggi), tetapi cabang remedial tetap nol — inilah perbedaan nyata line lawan branch coverage.
  5. Baris kategoriNilai(45): satu test tambahan menutup cabang terakhir; coverage naik dari 2/3 ke 3/3 tanpa menyentuh method produksi sama sekali — persis bagaimana menambah test menggerakkan angka JaCoCo.

Membaca Laporan JaCoCo #

Di proyek Maven, JaCoCo dipasang sebagai plugin dan menulis laporan HTML pada target/site/jacoco/index.html:

xml Plugin JaCoCo di pom.xml
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.12</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Goal prepare-agent menyiapkan Java agent yang melakukan instrumentasi bytecode saat test berjalan; goal report merangkum data penghitung menjadi laporan. Laporan HTML mewarnai sumber: hijau untuk baris tercakup, kuning untuk baris dengan cabang parsial, merah untuk baris yang tak tersentuh. Kolom penting yang sering diabaikan pemula: Missed Branches — kolom inilah yang menunjukkan if yang hanya pernah bernilai satu arah, dan karenanya lebih berharga daripada persentase instruksi keseluruhan.

100% Coverage Bukan Jaminan #

Kesalahpahaman terbesar tentang coverage: angka 100% tidak berarti kode bebas bug, karena coverage hanya mengukur eksekusi, bukan verifikasi. Test tanpa assertion sama sekali mengeksekusi seluruh kode dan menaikkan coverage ke 100% tanpa memeriksa satu hasil pun. Test yang memeriksa hasil dengan salah (membandingkan nilai dengan dirinya sendiri) juga hijau dan terhitung tercakup. Sebaliknya, test bermutu yang menemukan bug justru menurunkan coverage sementara karena baris penyebabnya diperbaiki.

Alat yang menguji kualitas assertion secara langsung adalah mutation testing: alat seperti PIT mengubah kode produksi secara kecil (membalik operator perbandingan, menghapus panggilan) lalu memeriksa apakah ada test yang gagal. Mutasi yang selamat dari seluruh suite menandakan celah verifikasi — ukuran yang jauh lebih dekat ke “seberapa kuat test” daripada coverage.

Angka Sasaran yang Sehat #

Aturan praktis industri: coverage menyala rendah sebagai ambang batas kualitas (misalnya branch coverage minimal 70–80% pada modul inti), bukan sebagai target yang harus dipenuhi dengan cara apa pun. Fokuskan energi pada kode yang berubah: diff yang masuk pull request sebaiknya datang bersama test yang menaikkan coverage baris baru mendekati penuh. Sisakan waktu memeriksa kolom Missed Branches pada class dengan logika keputusan padat — satu cabang else yang tak teruji adalah satu perilaku yang belum punya spesifikasi yang bisa dijalankan.

Dengan coverage sebagai kompas, kita tahu wilayah mana yang belum teruji. Chapter berikutnya mengubah cara berpikir tentang data uji itu sendiri: dari memilih contoh satu per satu menjadi menyatakan properti yang berlaku untuk semua input.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

ContohCoverage.java

Unduh berkas
java
public class ContohCoverage {

    // instrumentasi manual: satu penghitung per cabang
    static final int[] kunjunganCabang = new int[3];

    static String kategoriNilai(int nilai) {
        if (nilai >= 80) {
            kunjunganCabang[0]++;
            return "unggul";
        }
        if (nilai >= 60) {
            kunjunganCabang[1]++;
            return "lulus";
        }
        kunjunganCabang[2]++;
        return "remedial";
    }

    static void laporan() {
        String[] label = {
            "nilai >= 80 -> unggul",
            "nilai >= 60 -> lulus",
            "lainnya     -> remedial"
        };
        int cabangTerkunjungi = 0;
        for (int i = 0; i < kunjunganCabang.length; i++) {
            System.out.printf("cabang %-24s : %d kunjungan%n", label[i], kunjunganCabang[i]);
            if (kunjunganCabang[i] > 0) {
                cabangTerkunjungi++;
            }
        }
        System.out.println("branch coverage : "
            + cabangTerkunjungi + "/" + kunjunganCabang.length + " cabang");
    }

    public static void main(String[] args) {
        System.out.println("== suite awal: test nilai 85 dan 72 ==");
        System.out.println("hasil : " + kategoriNilai(85) + ", " + kategoriNilai(72));
        laporan();

        System.out.println("== suite ditambah test nilai 45 ==");
        System.out.println("hasil : " + kategoriNilai(45));
        laporan();
    }
}