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
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:
php artisan test --coverageOutput yang Dihasilkan:
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:
pecl install pcov; php.ini
extension=pcov.soSeluruh 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 #
php artisan test --coverageOutput yang Dihasilkan:
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 lewatexpectException.Services/PoinLoyalitas ... 21..23 / 25.0%— baru 1 dari 4 baris dieksekusi;21..23adalah rentang baris yang belum tersentuh, tepat di tiga cabangmatchmiliktier().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:
vendor/phpunit/phpunit/phpunit --coverage-textOutput yang Dihasilkan (tanggal laporan dipangkas):
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:
| Metrik | Sebuah unit dihitung tuntas bila | PoinLoyalitas |
|---|---|---|
| Lines | barisnya dieksekusi | 1 dari 4 baris |
| Methods | seluruh baris di dalamnya dieksekusi | 1 dari 2 method |
| Classes | seluruh method di dalamnya tuntas | belum 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:
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);
}php artisan test --coverage tests/Unit/LaporanTanpaVerifikasiTest.phpOutput yang Dihasilkan:
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:
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.
php artisan test --coverage --min=80Output yang Dihasilkan:
────────────────────────────────────────────────────────────────────────────
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:
- name: Test dengan coverage minimum
run: php artisan test --coverage --min=50Pastikan 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
.