Suite test yang selalu hijau menyisakan satu pertanyaan: berapa banyak kode aplikasi yang benar-benar tersentuh olehnya? Test coverage menjawab dengan mengukur baris mana yang dieksekusi ketika test berjalan. Angkanya adalah sinyal, bukan tujuan — sebuah baris boleh jadi dieksekusi tanpa hasilnya pernah diperiksa. Pada chapter penutup section ini kita mengukur suite kecil kita, memasang driver coverage, lalu membaca laporannya secara kritis — dari arti tiap angka sampai jebakan mengejar 100%.

Skenario: Tiga Class, Tiga Tingkat Cakupan #

Kita siapkan sebaran yang jujur di app/Services. PriceCalculator kembali dari chapter Unit Test dengan data provider dan dua test exception — jalurnya tuntas diuji. LaporanPenjualan belum tersentuh test. Di tengahnya berdiri PoinLoyalitas yang baru punya satu test untuk poinDariTransaksi() — class inilah yang akan sering kita bongkar:

php
<?php

namespace App\Services;

class PoinLoyalitas
{
    /**
     * Hitung poin: satu poin per Rp10.000 transaksi.
     */
    public function poinDariTransaksi(int $total): int
    {
        return intdiv($total, 10000);
    }

    /**
     * Tentukan tier anggota berdasarkan poin.
     */
    public function tier(int $poin): string
    {
        return match (true) {
            $poin >= 1000 => 'gold',
            $poin >= 100 => 'silver',
            default => 'bronze',
        };
    }
}

Satu-satunya test saat ini memanggil poinDariTransaksi(); tier() dengan tiga cabang match-nya menunggu di baris 21–23. Karena phpunit.xml membatasi laporan ke direktori app, class bawaan seperti Models\User ikut terhitung.

Syarat yang Sering Terlupakan: Driver Coverage #

PHPUnit tidak menghitung coverage sendiri; pekerjaan itu milik driver — extension PHP yang menginstrumentasi eksekusi baris demi baris. Lingkungan tanpa driver ditolak mentah-mentah:

bash
php artisan test --coverage

Output yang Dihasilkan:

text
   ERROR  Code coverage driver not available. Did you install Xdebug or PCOV?

Error tersebut jujur: salah satu dari dua extension berikut wajib terpasang. PCOV dibangun khusus untuk coverage sehingga cepat dan hemat memori — pilihan utama untuk suite sehari-hari. Xdebug adalah debugger umum yang jauh lebih berat. Pemasangan PCOV lewat PECL hanya dua langkah:

bash
pecl install pcov
ini
; php.ini
extension=pcov.so

Seluruh laporan pada chapter ini dijalankan di PHP 8.4 CLI dengan PCOV aktif — angka yang kita baca berikutnya adalah hasil eksekusi nyata, bukan ilustrasi.

Membaca Laporan Pertama #

bash
php artisan test --coverage

Output yang Dihasilkan:

text
   PASS  Tests\Unit\ExampleTest
  ✓ that true is true

   PASS  Tests\Unit\PoinLoyalitasTest
  ✓ poin satu per sepuluh ribu

   PASS  Tests\Unit\PriceCalculatorTest
  ✓ total subtotal dikurangi diskon with data set "tanpa diskon, satu item"
  ✓ total subtotal dikurangi diskon with data set "tanpa diskon, tiga item"
  ✓ total subtotal dikurangi diskon with data set "diskon sepuluh persen"
  ✓ total subtotal dikurangi diskon with data set "diskon maksimal lima puluh persen"
  ✓ kuantitas nol melempar exception
  ✓ diskon diluar rentang melempar exception

   PASS  Tests\Feature\ExampleTest
  ✓ the application returns a successful response                        0.09s

  Tests:    9 passed (11 assertions)
  Duration: 0.17s

  Http/Controllers/Controller ......................................... 100.0%
  Models/User ........................................................... 0.0%
  Providers/AppServiceProvider ........................................ 100.0%
  Services/LaporanPenjualan ............................................. 0.0%
  Services/PoinLoyalitas ...................................... 21..23 / 25.0%
  Services/PriceCalculator ............................................ 100.0%
  ────────────────────────────────────────────────────────────────────────────
                                                                 Total: 40.7 %

Tabel di bagian bawah meratakan setiap file di bawah app; persentasenya adalah baris yang dieksekusi saat seluruh suite berjalan. Tiga baris paling informatif:

  • Services/PriceCalculator ... 100.0% — semua jalur dijalankan: happy path lewat data provider, dua jalur exception lewat expectException.
  • Services/PoinLoyalitas ... 21..23 / 25.0% — baru 1 dari 4 baris dieksekusi; 21..23 adalah rentang baris yang belum tersentuh, tepat di tiga cabang match milik tier().
  • Services/LaporanPenjualan ... 0.0% — class ini bahkan tidak pernah dimuat oleh test mana pun.

Satu jebakan baca: Http/Controllers/Controller tampil 100.0% bukan karena diuji, melainkan karena file tersebut tanpa baris logika sehingga dilaporkan penuh. Total: 40.7% berarti 11 dari 27 baris dieksekusi.

Lines, Methods, dan Classes #

artisan test menampilkan satu angka per file. PHPUnit sendiri bisa merinci tiga metrik sekaligus lewat --coverage-text:

bash
vendor/phpunit/phpunit/phpunit --coverage-text

Output yang Dihasilkan (tanggal laporan dipangkas):

text
PHPUnit 12.5.39 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.4.8 with PCOV 1.0.12

OK (9 tests, 11 assertions)

Code Coverage Report:

 Summary:
  Classes: 40.00% (2/5)
  Methods: 50.00% (4/8)
  Lines:   40.74% (11/27)

App\Providers\AppServiceProvider
  Methods: 100.00% ( 2/ 2)   Lines: 100.00% ( 2/ 2)
App\Services\PoinLoyalitas
  Methods:  50.00% ( 1/ 2)   Lines:  25.00% ( 1/ 4)
App\Services\PriceCalculator
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% ( 8/ 8)

Ketiganya dihitung bertingkat, dari Lines ke Classes makin ketat:

MetrikSebuah unit dihitung tuntas bilaPoinLoyalitas
Linesbarisnya dieksekusi1 dari 4 baris
Methodsseluruh baris di dalamnya dieksekusi1 dari 2 method
Classesseluruh method di dalamnya tuntasbelum lulus

File tanpa satu baris pun yang dieksekusi — seperti LaporanPenjualan — tidak dicetak di daftar detail, tetapi tetap dihitung di Summary. Dari lima class berlogika, baru dua tuntas: AppServiceProvider yang dijalankan setiap bootstrap, dan PriceCalculator.

Persentase Tinggi Bukan Bukti Perilaku Benar #

Coverage mengukur eksekusi, bukan verifikasi — dan mudah dibuktikan. Tambahkan satu test yang memanggil kedua method LaporanPenjualan tanpa memeriksa hasil apa pun:

php
public function test_laporan_dieksekusi_tanpa_verifikasi(): void
{
    $laporan = new LaporanPenjualan;

    $laporan->totalPerBulan([['2026-01', 500000], ['2026-02', 750000]]);
    $laporan->bulanTerbaik(['2026-01' => 500000, '2026-02' => 750000]);

    $this->assertTrue(true);
}
bash
php artisan test --coverage tests/Unit/LaporanTanpaVerifikasiTest.php

Output yang Dihasilkan:

text
   PASS  Tests\Unit\LaporanTanpaVerifikasiTest
  ✓ laporan dieksekusi tanpa verifikasi

  Tests:    1 passed (1 assertions)
  Duration: 0.07s

  Http/Controllers/Controller ......................................... 100.0%
  Models/User ........................................................... 0.0%
  Providers/AppServiceProvider .......................................... 0.0%
  Services/LaporanPenjualan ....................................... 31 / 88.9%
  Services/PoinLoyalitas ................................................ 0.0%
  Services/PriceCalculator .............................................. 0.0%
  ────────────────────────────────────────────────────────────────────────────
                                                                 Total: 27.6 %

Satu test tanpa verifikasi melonjakkan LaporanPenjualan dari 0% ke 88.9% — hanya baris throw di baris 31 yang tertinggal. Seandainya totalPerBulan() keliru menjumlahkan, test di atas tetap hijau, karena satu-satunya assertion memeriksa true. Metrik Methods bahkan masih 50% (1/2) karena baris itu belum dieksekusi.

Mutation testing memperkeras ukuran ini: Infection menyuntikkan bug kecil — menukar operator, menghapus panggilan — lalu menghitung berapa mutan yang ditangkap test. Suite bercakupan tinggi yang membiarkan mutan lolos hanya menjalankan kode, bukan mengujinya; disiplin assertion dari chapter Unit Test adalah penawarnya.

Laporan sebagai Daftar Tugas #

Balik sudut pandangnya: coverage rendah bukan aib, melainkan antrean pekerjaan. Petunjuk 21..23 menunjuk tiga cabang match di tier() yang belum teruji; satu test kecil menutupnya:

php
public function test_tier_berdasarkan_poin(): void
{
    $loyalitas = new PoinLoyalitas;

    $this->assertSame('bronze', $loyalitas->tier(99));
    $this->assertSame('silver', $loyalitas->tier(100));
    $this->assertSame('gold', $loyalitas->tier(1000));
}

Pada run berikutnya PoinLoyalitas naik ke 100.0% dan total suite ke 51.9%. Persentase ikut naik — tetapi yang bernilai adalah tiga assertion yang mengunci perilaku tier(), bukan angkanya.

Menjaga Angka dengan --min #

Opsi --min menjaga angka dari waktu ke waktu: eksekusi gagal bila total coverage berada di bawah ambang.

bash
php artisan test --coverage --min=80

Output yang Dihasilkan:

text
  ────────────────────────────────────────────────────────────────────────────
                                                                 Total: 40.7 %

   FAIL  Code coverage below expected: 40.7 %. Minimum: 80.0 %.

Perintah tersebut mengembalikan exit code 1 sehingga pipeline CI otomatis gagal. Pola yang sehat adalah ratchet: pasang ambang sedikit di atas angka saat ini (misalnya --min=50 untuk suite kita), naikkan saat coverage membaik, dan jangan menurunkannya. Di GitHub Actions, langkah berikut menjalankan mekanisme yang sama:

yaml
- name: Test dengan coverage minimum
  run: php artisan test --coverage --min=50

Pastikan runner memasang PCOV; action setup-php yang populer menyediakan input coverage: pcov untuk itu.

Coverage menutup section ini dengan pesan yang sama seperti pembukaannya: angka adalah sinyal. Laporan menunjukkan kode yang belum tersentuh dan menjadi pagar regresi di CI — tetapi kualitas test tetap hidup di dalam assertion, bukan di persentase. Dengan jaring pengaman test terpasang, kita siap melangkah ke lapisan pertahanan berikutnya: section Keamanan Aplikasi . Rujukan lengkap perintah artisan test tersedia di dokumentasi Testing Laravel .