Chapter sebelumnya membangun cara berpikirnya: tes harus cepat, terisolasi, deterministik, dan tersusun sebagai piramida dengan unit test sebagai alasnya. Kini kita turun ke dasar piramida itu dan benar-benar menulis unit test. Unit adalah bagian kode terkecil yang bisa diuji berdiri sendiri — satu fungsi atau satu class — dan unit test memeriksanya tanpa koneksi database, tanpa jaringan, tanpa berkas konfigurasi. Dalam chapter ini kita akan menulis tes pertama dengan assertion polos PHP, menata berkas dan penamaannya, menguji fungsi murni lengkap dengan kasus batasnya, lalu menjalankan semuanya dengan PHPUnit sungguhan — termasuk sengaja membuatnya gagal agar kita tahu cara membaca laporan kegagalannya.
Apa Itu Unit #
Unit adalah potongan perilaku terkecil yang masih berarti diuji: satu fungsi seperti hitungTotalBelanja(), atau satu method pada satu class. Batasnya diambil dari isolasi — unit test boleh memanggil unit beserta fungsi murni lain yang dipanggilnya, tetapi tidak boleh menjamah dunia luar: database, API, ber filesystem, clock, atau $_POST. Sebaliknya, kode yang butuh database atau HTTP untuk membuktikan sesuatu bukan lagi unit test melainkan integration test — tingkatan berikutnya di piramida.
| Kode yang diuji | Jenis tesnya |
|---|---|
hitungTotalBelanja(array $item) murni | Unit test |
formatRupiah(250000) murni | Unit test |
simpanPesanan() yang menulis ke PDO | Integration test |
| Alur checkout lengkap via HTTP | End-to-end test |
Batas ini bukan soal kesombongan puritan, melainkan kecepatan dan ketepatan diagnosis. Tes yang tidak menyentuh dunia luar berjalan dalam mikrodetik dan bisa dijalankan ribuan kali per hari; ketika gagal, hampir pasti logika di dalam fungsi itu yang salah, bukan koneksi wifi yang sedang putus. Mengecualikan dunia luar dari unit membuat kita melihat desain kode sendiri: fungsi yang mustahil diuji tanpa database adalah fungsi yang mencampur logika dengan akses data, dan pemisahan itulah perbaikan desain yang pertama.
Tes Pertama dengan Assertion Polos #
Sebelum ada PHPUnit, mari kita rasakan bahwa tes hanyalah kode PHP biasa. Ramuannya tiga bahan: fungsi yang diuji, harapan tertulis, dan perbandingan yang berteriak saat harapan dilanggar. Kita buat helper assertion mini bernama assertSama() lalu mengujinya pada fungsi belanja:
<?php
function assertSama(mixed $aktual, mixed $diharapkan, string $namaTes): void
{
if ($aktual !== $diharapkan) {
$pesan = "GAGAL [{$namaTes}] mengharapkan " . var_export($diharapkan, true)
. ", didapat " . var_export($aktual, true);
throw new RuntimeException($pesan);
}
echo "LULUS [{$namaTes}]\n";
}
function hitungTotalBelanja(array $itemBelanja): int
{
$total = 0;
foreach ($itemBelanja as $item) {
$total += $item["harga"] * $item["jumlah"];
}
return $total;
}
$itemBelanja = [
["nama" => "Kopi Gayo 250g", "harga" => 85000, "jumlah" => 2],
["nama" => "Gula Aren 500g", "harga" => 25000, "jumlah" => 1],
];
assertSama(hitungTotalBelanja($itemBelanja), 195000, "total dua item terhitung benar");
assertSama(hitungTotalBelanja([]), 0, "keranjang kosong menghasilkan nol");Output yang Dihasilkan:
LULUS [total dua item terhitung benar]
LULUS [keranjang kosong menghasilkan nol]Mari kita bedah perannya satu per satu. Fungsi assertSama() membandingkan aktual dengan diharapkan memakai !== (perbandingan ketat nilai sekaligus tipe — 195000 integer tidak boleh lolos sebagai "195000" string), dan saat gagal ia melempar exception berisi kedua nilai plus nama tes sehingga kegagalan terbaca tanpa debugger. Fungsi hitungTotalBelanja() adalah unit under test: murni, tanpa efek samping, hasilnya hanya ditentukan masukannya. Dua pemanggilan assertSama() adalah dua tes terpisah — satu perilaku normal, satu kasus batas keranjang kosong — dan keduanya lulus dalam sekejap.
Perhatikan pula urutan penulisan parameternya: aktual dulu, baru harapan. Konsistensi urutan ini membuat laporan kegagalan selalu terbaca dengan pola yang sama — “yang didapat X, padahal mengharapkan Y” — dan kebiasaan kecil seperti inilah yang membuat ratusan tes tetap mudah dibaca tim.
Struktur Berkas Uji dan Konvensi Penamaan #
Tes yang ditumpuk dalam satu berkas panjang cepat menjadi sulit dinavigasi, jadi proyek PHP menata lokasi dan nama berkasnya dengan konvensi baku: kode produksi di src/, tes di tests/, dan setiap berkas tes dinamai <NamaClass>Test.php untuk class atau <NamaFungsi>Test.php untuk kumpulan fungsi. PHPUnit mengenali berkas berakhiran Test.php ini secara otomatis:
proyek-toko/
├── src/
│ └── Perhitungan.php
└── tests/
└── PerhitunganTest.phpDi dalam berkas tes, nama method adalah dokumentasi pertama yang dibaca manusia saat tes gagal. Nama yang baik menyebut perilaku spesifik — pola test<Subjek><Perilaku> seperti testJumlahMenghitungTotal atau testHitungPajakMenolakPenghasilanNegatif:
| Nama kurang informatif | Nama yang menjelaskan perilaku |
|---|---|
test1(), testCek() | testTotalDuaItemTerhitungBenar() |
testHitungTotal() | testKeranjangKosongMenghasilkanNol() |
testError() | testPersenDiskonDiLuar0Sampai100Ditolak() |
Kolom kiri memaksa pembaca membuka isi method untuk tahu apa yang diperiksa; kolom kanan bercerita sendiri pada laporan kegagalan, dan pada saat bersamaan menjadi daftar perilaku yang dikontrakkan fungsi tersebut. Aturan praktisnya: jika nama tes membutuhkan kata “dan”, tes itu kemungkinan besar sedang memeriksa dua perilaku sekaligus — pecah menjadi dua tes.
Satu Perilaku per Tes #
Prinsip penulisan terpenting di level unit: satu tes memeriksa satu perilaku. Tampak memboroskan — kenapa tidak sekaligus memeriksa total, kasus kosong, dan format rupiah dalam satu method? — tetapi hitungannya terbalik. Ketika satu method raksasa gagal, laporannya hanya berkata “ada yang salah di suatu tempat di antara tiga pemeriksaan ini”, dan pembaca harus menyelam isi method untuk mencari yang mana. Ketika tes kecil testKeranjangKosongMenghasilkanNol() gagal, lokasi masalahnya sudah disebut namanya.
Tes satu-perilaku juga memberi presisi pada momen refactoring: Anda bisa melihat persis perilaku mana yang berubah karena tes-tes yang lain tetap hijau. Dan karena tiap tes menyiapkan datanya sendiri, kegagalan satu tes tidak menular ke tetangganya — ingat ciri terisolasi dari chapter sebelumnya. Dua puluh tes kecil yang masing-masing jelas bernilai itu bukan pemborosan baris; barisnya justru jauh lebih murah daripada waktu tim yang habis untuk menebak-nebak laporan kegagalan yang samar.
Mulai dari Fungsi Murni: Studi Kasus Pajak #
Target pertama yang paling menyenangkan untuk diuji adalah fungsi murni — hasil hanya ditentukan masukan, tanpa jam, tanpa acak, tanpa state. Fungsi pajak progresif adalah contoh yang bagus karena penuh kasus batas. Berikut implementasinya bersama tes-tes batasnya:
<?php
function assertSama(mixed $aktual, mixed $diharapkan, string $namaTes): void
{
if ($aktual !== $diharapkan) {
$pesan = "GAGAL [{$namaTes}] mengharapkan " . var_export($diharapkan, true)
. ", didapat " . var_export($aktual, true);
throw new RuntimeException($pesan);
}
echo "LULUS [{$namaTes}]\n";
}
function hitungPajakTahunan(float $penghasilanKenaPajak): float
{
if ($penghasilanKenaPajak <= 0.0) {
return 0.0;
}
if ($penghasilanKenaPajak <= 60_000_000) {
return round($penghasilanKenaPajak * 0.05, 2);
}
if ($penghasilanKenaPajak <= 250_000_000) {
return round($penghasilanKenaPajak * 0.15 - 6_000_000, 2);
}
return round($penghasilanKenaPajak * 0.25 - 31_000_000, 2);
}
$kasusBatas = [
["penghasilan nol", 0.0, 0.0],
["penghasilan negatif diabaikan", -5000.0, 0.0],
["tepat di batas atas bracket pertama", 60_000_000.0, 3_000_000.0],
["tepat di batas atas bracket kedua", 250_000_000.0, 31_500_000.0],
];
foreach ($kasusBatas as [$nama, $penghasilan, $pajakDiharapkan]) {
assertSama(hitungPajakTahunan($penghasilan), $pajakDiharapkan, $nama);
}Output yang Dihasilkan:
LULUS [penghasilan nol]
LULUS [penghasilan negatif diabaikan]
LULUS [tepat di batas atas bracket pertama]
LULUS [tepat di batas atas bracket kedua]Empat kasus pada tabel sengaja memilih titik-titik paling rawan kesalahan. Penghasilan nol dan negatif menguji pintu masuk fungsi; tepat 60_000_000 menguji sambungan bracket pertama-kedua (harus 3_000_000, bukan hasil rumus bracket yang salah apply); tepat 250_000_000 menguji sambungan kedua-ketiga dengan nilai 31_500_000. Kesalahan off-by-one pada pemakaian < versus <= di kondisi bracket hampir selalu lolos dari pengujian nilai acak, tetapi tertangkap oleh kasus tepat-di-batas seperti ini — itulah alasan kasus batas selalu ditulis eksplisit, bukan dilimpahkan pada keberuntungan.
Kasus Batas yang Wajib Dicoba #
Menulis kasus normal itu mudah; bug justru bersembunyi di tepian. Berikut kategori tepian yang layak menjadi checklist setiap kali Anda menulis unit test, masing-masing dengan contoh nyata dari chapter ini dan sekitarnya:
| Kategori | Contoh |
|---|---|
| Kosong dan nol | Keranjang belanja [], penghasilan 0 |
| Tepat di batas aturan | Penghasilan tepat 60_000_000 di ambang bracket |
| Nilai ekstrem | ID pesanan mendekati PHP_INT_MAX |
| Input tidak valid | orderId berisi "ABC-1024", persen diskon 150 |
| Duplikasi dan pengulangan | Job yang sama diproses dua kali (idempotency) |
Dua baris terakhir tabel menghubungkan kembali ke dunia asynchronous: input tidak valid adalah sumber kegagalan permanen pada chapter Job Failure Handling , dan pengulangan adalah inti pembahasan idempotency. Unit test adalah tempat termurah membuktikan perilaku tersebut — sebelum kode berhadapan dengan queue dan worker sungguhan.
Menjalankan Tes dengan PHPUnit #
Assertion polos membantu memahami hakikat sebuah tes, tetapi di dunia industri kita memakai alat standar: PHPUnit. Perilaku yang sama kini ditulis sebagai class yang mewarisi TestCase, dengan method test* ber-void, dan perbandingan digantikan method $this->assertSame():
<?php
use PHPUnit\Framework\TestCase;
function hitungTotalBelanja(array $itemBelanja): int
{
$total = 0;
foreach ($itemBelanja as $item) {
$total += $item["harga"] * $item["jumlah"];
}
return $total;
}
final class HitungTotalBelanjaTest extends TestCase
{
public function testTotalDuaItemTerhitungBenar(): void
{
$itemBelanja = [
["nama" => "Kopi Gayo 250g", "harga" => 85000, "jumlah" => 2],
["nama" => "Gula Aren 500g", "harga" => 25000, "jumlah" => 1],
];
$this->assertSame(195000, hitungTotalBelanja($itemBelanja));
}
public function testKeranjangKosongMenghasilkanNol(): void
{
$this->assertSame(0, hitungTotalBelanja([]));
}
}Berkas ini juga tersedia sebagai tests/HitungTotalBelanjaTest.php pada bundle chapter agar dapat Anda unduh dan jalankan langsung. Menjalankannya tidak butuh proyek Composer — cukup arahkan PHPUnit ke berkasnya:
php phpunit.phar --no-progress tests/HitungTotalBelanjaTest.phpKami benar-benar menjalankan perintah tersebut pada lingkungan Chapter ini (PHP 8.4.23, PHPUnit 12.5.39), dan berikut output nyatanya — termasuk hasil berkas bundle yang sama: OK (2 tests, 2 assertions):
PHPUnit 12.5.39 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.23
Time: 00:00.001, Memory: 28.05 MB
OK (2 tests, 2 assertions)Lima baris terakhir itulah yang paling sering Anda baca seumur karier: PHPUnit memperkenalkan dirinya dengan versi, mencatat runtime yang dipakai, lalu meringkas hasil — dua tes dijalankan, dua assertion dievaluasi, semuanya lulus (OK). Angka Time dan Memory bervariasi antar mesin dan antar jalan, jadi jangan dikhawatirkan; baris Configuration: none juga belum muncul karena proyek ini belum punya berkas phpunit.xml — kita akan menyusunnya di chapter Menginstal PHPUnit
. Opsi --no-progress kita pakai agar output stabil dan mudah dicatat; tanpa opsi itu PHPUnit juga menampilkan progress bar animasi di terminal.
Membaca Tes yang Gagal #
Tes yang selalu lulus belum membuktikan apa-apa tentang dirinya sendiri — bisa jadi ia bahkan tidak memeriksa apa yang kita kira. Latihan wajibnya: sengaja gagalkan. Ubah harapan pada testTotalDuaItemTerhitungBenar dari 195000 menjadi 200000, jalankan ulang, dan baca bentuk laporan kegagalannya:
php phpunit.phar --no-progress tests/HitungTotalBelanjaTest.phpPHPUnit 12.5.39 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.23
Time: 00:00.001, Memory: 28.05 MB
There was 1 failure:
1) HitungTotalBelanjaTest::testTotalDuaItemTerhitungBenar
Failed asserting that 195000 is identical to 200000.
/tmp/a94/gagal/HitungTotalBelanjaTest.php:24
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.Laporan kegagalan tersebut punya anatomi yang tetap sama di semua proyek PHPUnit, dan paten dibaca dari atas ke bawah. Nama lengkap tes yang gagal — HitungTotalBelanjaTest::testTotalDuaItemTerhitungBenar — langsung menunjuk perilaku mana yang dilanggar, terima kasih pada disiplin penamaan dari awal chapter. Baris Failed asserting that 195000 is identical to 200000 menyandingkan aktual dan harapan dalam satu kalimat; perhatikan bahwa assertSame sengaja menyebut “identical” karena membandingkan nilai sekaligus tipe. Baris path :24 menunjuk baris assertion yang tepat (path /tmp/a94/gagal/... adalah lokasi sementara salinan berkas saat kami mendemokan kegagalan; pada proyek Anda akan menunjuk tests/HitungTotalBelanjaTest.php). Terakhir, ringkasan Tests: 2, Assertions: 2, Failures: 1 menjelaskan bahwa hanya satu dari dua tes yang gagal — sesama penghuni suite tetap terisolasi, dan PHPUnit mengembalikan exit code 1 sehingga pipeline CI pun tahu build ini gagal. Kembalikan angkanya ke 195000, dan layar kembali hijau.
Ringkasan #
Unit adalah potongan perilaku terkecil yang diuji dalam isolasi total dari dunia luar, dan unit test-nya ditulis satu-perilaku-per-tes dengan nama method yang bercerita. Assertion polos memperlihatkan bahwa tes hanyalah perbandingan ketat yang berteriak nama tesnya saat dilanggar; fungsi murni seperti hitungPajakTahunan() adalah target pertama terbaik, dengan kasus batas di tepi aturan yang paling sering menyimpan bug. PHPUnit kemudian membungkus semua itu menjadi class TestCase dengan laporan OK (2 tests, 2 assertions) saat sehat dan anatomi kegagalan yang bisa dibaca dari nama, assertion, hingga baris. Langkah berikutnya adalah membawa alat ini masuk ke proyek sungguhan — instalasi via Composer, autoloading, dan berkas konfigurasi phpunit.xml — pada chapter Menginstal PHPUnit
.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
tests/HitungTotalBelanjaTest.php
Unduh berkas<?php
use PHPUnit\Framework\TestCase;
function hitungTotalBelanja(array $itemBelanja): int
{
$total = 0;
foreach ($itemBelanja as $item) {
$total += $item["harga"] * $item["jumlah"];
}
return $total;
}
final class HitungTotalBelanjaTest extends TestCase
{
public function testTotalDuaItemTerhitungBenar(): void
{
$itemBelanja = [
["nama" => "Kopi Gayo 250g", "harga" => 85000, "jumlah" => 2],
["nama" => "Gula Aren 500g", "harga" => 25000, "jumlah" => 1],
];
$this->assertSame(195000, hitungTotalBelanja($itemBelanja));
}
public function testKeranjangKosongMenghasilkanNol(): void
{
$this->assertSame(0, hitungTotalBelanja([]));
}
}