Sejauh ini kita menulis kode dan memastikannya bekerja dengan cara yang paling mahal: menjalankan manual, menatap layar, dan berharap tidak ada yang rusak. Cara itu berhasil sekali, tetapi gagal pada percobaan kedua — karena setiap perubahan kode menuntut kita mengulang seluruh ritual pemeriksaan, dan ingatan manusia tidak dirancang untuk itu. Software testing mengganti ritual tersebut dengan kode yang memeriksa kode: sekali ditulis, bisa dijalankan ribuan kali tanpa bosan. Section ini dimulai dari fondasi berpikarnya — mengapa menguji, tingkatan-tingkatan pengujian, ciri tes yang sehat, dan pola penulisan yang membuat tes mudah dibaca — sebelum kita menyentuh PHPUnit pada chapter-chapter berikutnya.

Mengapa Kode Perlu Diuji #

Manfaat pengujian paling sering dijual sebagai “mencegah bug”, tetapi manfaat pertamanya yang sesungguhnya adalah mencegah bug kembali. Setiap perbaikan yang pernah Anda tulis hanya permanen kalau ada tes yang akan berteriak begitu regression yang sama muncul lagi; tanpa tes, bug favorit aplikasi punya kebiasaan kembali tiga bulan kemudian lewat refactor yang terasa aman. Tester otomatis ini pula yang memberi keberanian untuk menyentuh kode lama — tim tanpa tes memperlakukan kode berumur enam bulan seperti benda purbakala yang tidak boleh diganggu.

Manfaat kedua lebih halus: tes memberi umpan balik desain. Kode yang sulit diuji hampir selalu kode yang terlalu menggenggam — fungsi yang membaca $_POST langsung, class yang membangun koneksi database di dalam constructor, logika yang tertanam di tengah HTML. Saat Anda terpaksa menulis tes untuk kode seperti itu, rasa sakitnya adalah diagnosis: “terlalu banyak tanggung jawab sekaligus”. Memperbaiki desain agar bisa diuji membuat kode lebih modular, dan modular adalah fondasi semua arsitektur bagus.

Manfaat ketiga: tes adalah dokumentasi yang tidak pernah kedaluwarsa. Komentar bisa berbohong karena tidak pernah diverifikasi, tetapi tes bernama testDiskonNolUntukPelangganBaru terbukti jujur — ia gagal begitu perilakunya berubah. Pembaca baru kode Anda mempelajari cara pakai sebuah fungsi paling cepat lewat tes-nya: masukan apa yang wajar, output apa yang diharapkan, dan kasus aneh apa yang sudah pernah dipikirkan.

Tiga Tingkatan Pengujian #

Tidak semua tes bernilai sama. Sebuah tes yang memanggil satu fungsi murni berjalan dalam mikrodetik, sedangkan tes yang membuka browser dan mengklik tombol checkout bisa memakan belasan detik. Dunia pengujian menata spektrum ini ke dalam tingkatan yang sering digambar sebagai piramida:

TingkatanCakupanKecepatanKegunaan utama
Unit testSatu fungsi/class terisolasiMikrodetik–milidetikMemverifikasi logika terkecil
Integration testBeberapa komponen bersama (database, queue)Milidetik–detikMemverifikasi kerja sama antar bagian
End-to-end testAplikasi utuh lewat antarmuka penggunaDetik–menitMemverifikasi alur bisnis dari luar

Diagram piramida tersebut membaca dari bawah: mayoritas investasi tes Anda sebaiknya berada di unit test karena cepat, murah, dan menunjuk lokasi bug dengan presisi; integration test berjumlah sedang untuk memverifikasi sambungan (query benar-benar jalan di database, job benar-benar diproses worker); dan end-to-end paling sedikit karena paling mahal — lambat, rapuh terhadap perubahan tampilan, dan saat gagal hanya berkata “ada yang salah di suatu tempat”. Bentuk piramida bukan dogma; aplikasi kritikal bisa punya piramida yang lebih gemuk di tengah. Yang penting dipahami: menambah end-to-end test bukanlah satu-satunya cara meningkatkan keyakinan, dan menumpuknya justru melambatkan seluruh tim.

Ciri Tes yang Baik #

Tes juga kode — dan karenanya bisa buruk. Tiga ciri memisahkan tes yang sehat dari tes yang menjadi beban. Cepat: suite yang butuh 40 menit tidak akan dijalankan siapa pun sebelum commit, dan tes yang tidak dijalankan sama saja tidak ada. Terisolasi: satu tes tidak boleh bergantung pada sisa tes lain — tidak berbagi state, tidak bergantung urutan jalan — sehingga kegagalannya bisa ditafsirkan sendiri tanpa perlu membaca seluruh suite. Deterministik: hasil tes sama setiap kali dijalankan pada kode yang sama; tes yang kadang lulus kadang gagal (flaky) melatih tim mengabaikan alarm merah persis seperti yang kita bahas tentang peringatan worker di chapter Job Failure Handling .

CiriPertanyaan pemeriksaanGejala pelanggaran
CepatApakah suite penuh selesai dalam hitungan detik–menit?Tes hanya dijalankan seminggu sekali
TerisolasiApakah tiap tes bisa dijalankan sendiri?Tes lulus berurutan tapi gagal terpisah
DeterministikApakah hasil selalu sama pada kode sama?“Coba jalankan lagi, biasanya lulus”

Ciri-ciri tersebut saling menopang: tes yang tidak terisolasi cenderung flaky, tes yang flaky membuat suite lambat dipercaya, dan suite yang tidak dipercaya kembali ke ritual penatapan layar. Kabar baiknya, ketiganya murah dijaga sejak awal — mayoritas resepnya adalah menulis tes untuk unit kecil yang tidak menyentuh jaringan, jam, atau angka acak.

Pola Arrange-Act-Assert #

Begitu kita mulai menulis tes, struktur isinya mengikuti satu pola yang telah terbukti paling mudah dibaca: arrange (siapkan keadaan), act (jalankan aksi yang diuji), assert (periksa hasilnya). Pola ini biasa disingkat AAA, dan kekuatannya ada pada keterbacaan: pembaca langsung tahu bagian mana masukan, mana perilaku, mana harapan. Mari kita tulis tes pertama kita — masih tanpa PHPUnit, cukup PHP polos dan perbandingan biasa:

php
<?php

function hitungDiskon(float $totalBelanja, int $persenDiskon): float
{
    if ($persenDiskon < 0 || $persenDiskon > 100) {
        throw new InvalidArgumentException("persenDiskon harus di antara 0 dan 100");
    }

    return round($totalBelanja * $persenDiskon / 100, 2);
}

// Arrange: siapkan masukan dan hasil yang diharapkan
$totalBelanja = 250000.0;
$persenDiskon = 20;
$hasilDiharapkan = 50000.0;

// Act: jalankan kode yang sedang diuji
$hasilAktual = hitungDiskon($totalBelanja, $persenDiskon);

// Assert: bandingkan hasil aktual dengan hasil yang diharapkan
if (abs($hasilAktual - $hasilDiharapkan) < 0.001) {
    echo "LULUS: diskon {$persenDiskon}% dari {$totalBelanja} = {$hasilAktual}\n";
} else {
    echo "GAGAL: mengharapkan {$hasilDiharapkan}, didapat {$hasilAktual}\n";
    exit(1);
}

Output yang Dihasilkan:

text
LULUS: diskon 20% dari 250000 = 50000

Empat bagian kode tersebut masing-masing punya peran tegas. Fungsi hitungDiskon() adalah unit under test — satu perilaku murni tanpa database atau jam. Bagian arrange menyiapkan masukan (250000, 20%) sekaligus harapan (50000) berdampingan sehingga pembaca menangkap maksud tes dalam sekali lihat. Bagian act memanggil fungsi tepat satu kali. Bagian assert membandingkan hasil dengan toleransi 0.001 untuk bilangan desimal — membandingkan float dengan === mentah-mentah adalah sumber kegagalan palsu karena 0.1 + 0.2 tidak persis 0.3 di dunia biner. Perhatikan pula exit(1) pada cabang gagal: skrip tes yang baik menyampaikan kegagalan lewat exit code, bukan hanya teks merah — kebiasaan yang nanti membuat PHPUnit, CI, dan berbagai alat bisa bekerja tanpa mem-parsing tulisan kita.

Satu Kasus, Satu Baris: Pengujian Berbasis Data #

Satu perilaku jarang cukup diuji satu masukan. Diskon bisa dites dengan nilai normal, nol, maksimal, dan input tidak valid. Menulis empat tes terpisah dengan struktur penuh akan menghasilkan pengulangan; solusinya adalah menyusun kasus sebagai tabel data dan memutarnya dalam satu loop — bentuk awal dari data provider yang akan kita temui resmi di PHPUnit:

php
<?php

function hitungDiskon(float $totalBelanja, int $persenDiskon): float
{
    if ($persenDiskon < 0 || $persenDiskon > 100) {
        throw new InvalidArgumentException("persenDiskon harus di antara 0 dan 100");
    }

    return round($totalBelanja * $persenDiskon / 100, 2);
}

$kasusUji = [
    "diskon normal"   => [250000.0, 20, 50000.0],
    "tanpa diskon"    => [150000.0, 0, 0.0],
    "diskon maksimal" => [99900.0, 100, 99900.0],
];

foreach ($kasusUji as $namaKasus => [$totalBelanja, $persenDiskon, $hasilDiharapkan]) {
    $hasilAktual = hitungDiskon($totalBelanja, $persenDiskon);
    $lulus = abs($hasilAktual - $hasilDiharapkan) < 0.001;

    echo ($lulus ? "LULUS" : "GAGAL") . ": {$namaKasus}\n";
}

// kasus ekstra: input tidak valid harus melempar exception
try {
    hitungDiskon(100000.0, 150);
    echo "GAGAL: persen tidak valid\n";
    exit(1);
} catch (InvalidArgumentException $kegagalan) {
    echo "LULUS: persen tidak valid — pesan: " . $kegagalan->getMessage() . "\n";
}

Output yang Dihasilkan:

text
LULUS: diskon normal
LULUS: tanpa diskon
LULUS: diskon maksimal
LULUS: persen tidak valid — pesan: persenDiskon harus di antara 0 dan 100

Struktur tabel $kasusUji memberi setiap kasus nama yang bisa dibaca manusia — saat sebuah baris gagal, laporannya menyebut “diskon maksimal” alih-alih baris kode nomor sekian. Kasus terakhir memperlihatkan pola menguji kegagalan: perilaku “menolak input tidak valid” juga adalah perilaku yang layak dibuktikan, dengan try/catch yang menandai lulus justru saat exception yang diharapkan benar-benar muncul. Aturan penulisan tabelnya sederhana: satu baris menjelaskan satu skenario lengkap dari masukan hingga harapan, tanpa perlu membaca kode loop untuk memahaminya.

Siklus Red-Green-Refactor #

Dengan bekal pola AAA, kita kini bisa membicarakan cara kerja pengembangan berkendali tes (test-driven development). Alurnya dikenal sebagai red-green-refactor: tulis dulu tes yang gagal (red) karena fiturnya belum ada; tulis kode paling sederhana yang membuat tes lulus (green); lalu rapikan kode dengan tenang karena tes menjaga perilakunya (refactor); ulangi. Setiap putaran hanya berlangsung beberapa menit.

Kekuatan siklus ini bukan seremonial. Fase red memaksa kita mendefinisikan perilaku sebelum implementasi — definisi yang bisa dikritik dan dikoreksi murah, sebelum satu baris logika ditulis. Fase green mencegah kita menulis kemampuan yang belum diperlukan. Fase refactor memberi ruang aman untuk memperbaiki kualitas tanpa takut merusak, dan disinilah desain kode Anda sungguh-sungguh naik kelas. Anda tidak wajib menulis semua kode dengan cara ini — banyak tim menulis tes setelah kodenya — tetapi mencoba TDD pada satu-dua fungsi murni adalah latihan yang sangat menajamkan naluri “apa sebenarnya yang ingin saya buktikan?”.

Biaya Jujur dan Mitos Coverage #

Berkat jujur harus ditutup jujur: tes punya biaya. Menulis dan merawatnya memakan waktu, tes yang ditulis terburu-buru akan menghambat refactor alih-alih menolongnya, dan suite yang membesar tanpa pengelolaan menjadi lambat lalu diabaikan. Karena itu kualitas lebih penting daripada kuantitas: sepuluh tes yang menembak perilaku kritis lebih berharga daripada seratus tes yang menguji getter. Iklan angka pun layak dicurigai — metrik paling sering dipakai untuk pamer adalah coverage, persentase baris kode yang dieksekusi tes. Coverage mengukur eksekusi, bukan pemeriksaan:

php
<?php

// Tes tanpa assertion: baris hitungDiskon() tetap "tereksekusi"
// sehingga coverage naik, tetapi tidak ada perilaku yang dibuktikan.
function tesTanpaPemeriksaanHasil(): void
{
    hitungDiskon(250000.0, 20); // hasil dibuang, tidak diperiksa
}

Potongan di atas menaikkan coverage fungsi hitungDiskon() menjadi 100% tanpa membuktikan apa pun — hasil pemanggilan dibuang begitu saja. Coverage tetap berguna sebagai alat diagnostik (“modul laporan tidak pernah tersentuh tes sama sekali”), tetapi buruk sebagai target yang dipuji. Ukuran yang lebih jujur adalah kepercayaan: ketika tes gagal, apakah Anda percaya ada bug sungguhan; ketika tes lulus, apakah Anda berani deploy.

Ringkasan #

Pengujian mengunci perilaku kode: mencegah bug kembali, memaksa desain yang bisa diuji, dan menjadi dokumentasi yang diverifikasi setiap hari. Investasi terbaik bertumpuk di dasar piramida — unit test yang cepat, terisolasi, dan deterministik — dengan integration dan end-to-end secukupnya di atasnya. Struktur isinya mengikuti pola arrange-act-assert, kasus dikelola sebagai tabel data bernama, dan siklus red-green-refactor menjadi cara kerja opsional yang menajamkan definisi perilaku. Mitos coverage kita bongkar dengan satu potongan kode tanpa assertion. Fondasi berpikir ini cukup — saatnya menulis tes sungguhan dengan alat standar industri, dan untuk itu kita mulai dari memahami unit itu sendiri pada chapter Unit Testing .

Referensi #