Testing dengan PHPUnit
Mengapa menulis tes otomatis, memasang PHPUnit lewat Composer, menulis test case, assertion, data provider, menguji exception, dan test double (mock).
Mengapa menguji?
Tanpa tes, setiap perubahan kode adalah pertaruhan: kamu memperbaiki satu fitur dan tanpa sadar merusak yang lain. Tes otomatis adalah program kecil yang memeriksa program utamamu dalam hitungan detik, setiap kali kamu mau. Hasilnya: berani mengubah kode, bug cepat ketahuan, dan tes berfungsi sebagai dokumentasi cara pakai.
Memasang PHPUnit
composer require --dev phpunit/phpunit ^11
vendor/bin/phpunit --version
Struktur proyek yang umum:
proyek/
├── composer.json
├── phpunit.xml
├── src/Kalkulator.php
└── tests/KalkulatorTest.php
Contoh phpunit.xml minimal:
<phpunit bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="app"><directory>tests</directory></testsuite>
</testsuites>
</phpunit>
Tes pertamamu
Kode yang diuji (src/Kalkulator.php):
namespace App;
class Kalkulator
{
public function bagi(int $a, int $b): float
{
if ($b === 0) {
throw new \InvalidArgumentException('Pembagi tidak boleh nol');
}
return $a / $b;
}
}
Tesnya (tests/KalkulatorTest.php):
namespace Tests;
use App\Kalkulator;
use PHPUnit\Framework\TestCase;
class KalkulatorTest extends TestCase
{
public function testBagiMengembalikanHasilYangBenar(): void
{
$kalkulator = new Kalkulator();
$hasil = $kalkulator->bagi(10, 4);
$this->assertSame(2.5, $hasil);
}
public function testBagiDenganNolMelemparException(): void
{
$this->expectException(\InvalidArgumentException::class);
(new Kalkulator())->bagi(1, 0);
}
}
Jalankan: vendor/bin/phpunit. Titik hijau berarti lulus, F berarti gagal.
Aturan penamaan
- Class tes berakhiran
Testdanextends TestCase. - Method tes berawalan
test(atau diberi atribut#[Test]), bertipevoid, danpublic. - Nama method menjelaskan perilaku yang diharapkan.
Pola Arrange – Act – Assert
| Tahap | Isi |
|---|---|
| Arrange | Siapkan objek dan data |
| Act | Jalankan satu aksi yang diuji |
| Assert | Periksa hasilnya |
Assertion yang sering dipakai
| Assertion | Memeriksa |
|---|---|
assertSame($harapan, $aktual) | Nilai dan tipe sama (===), pilihan utama |
assertEquals(...) | Nilai sama (==, longgar) |
assertTrue / assertFalse | Boolean |
assertNull / assertNotNull | Null |
assertCount(n, $array) | Jumlah elemen |
assertContains($x, $array) | Elemen ada di array |
assertInstanceOf(Kelas::class, $obj) | Tipe objek |
expectException(Kelas::class) | Pernyataan berikutnya harus melempar exception |
Data provider: satu tes, banyak kasus
use PHPUnit\Framework\Attributes\DataProvider;
#[DataProvider('kasusBagi')]
public function testBagi(int $a, int $b, float $harapan): void
{
$this->assertSame($harapan, (new Kalkulator())->bagi($a, $b));
}
public static function kasusBagi(): array
{
return [
'habis dibagi' => [10, 2, 5.0],
'desimal' => [1, 4, 0.25],
'negatif' => [-9, 3, -3.0],
];
}
setUp: persiapan sebelum tiap tes
private Kalkulator $kalkulator;
protected function setUp(): void
{
$this->kalkulator = new Kalkulator(); // objek baru untuk setiap tes
}
Test double: mengganti dependency
Saat menguji Notifikasi yang memakai PengirimEmail (interface), kita tidak mau benar-benar mengirim email. Buat mock:
public function testKirimSelamatDatang(): void
{
$pengirim = $this->createMock(PengirimEmail::class);
$pengirim->expects($this->once())
->method('kirim')
->with('budi@mail.com', $this->stringContains('Selamat'))
->willReturn(true);
$layanan = new Notifikasi($pengirim);
$this->assertTrue($layanan->sambut('budi@mail.com'));
}
Ini alasan utama kita memakai interface dan dependency injection di bab OOP: class jadi mudah diuji.
Tes yang baik
- Cepat dan mandiri (urutan tes tidak boleh saling bergantung).
- Menguji satu perilaku per tes, dengan nama yang menjelaskannya.
- Menguji kasus batas: nol, kosong, negatif, nilai maksimum, input salah.
- Jangan menguji detail internal; uji hasil yang dilihat dari luar.
Error umum
No tests found in class→ nama method tidak berawalantestatau tidakpublic.Class "App\Kalkulator" not found→ autoload PSR-4 belum diatur dicomposer.json; jalankancomposer dump-autoload.Failed asserting that 2.5 is identical to '2.5'→assertSamememeriksa tipe; samakan tipenya.