Chapter Assertions
memberi kita alat ukur yang tajam, tetapi satu godaan muncul begitu test bertambah: menyalin-tempel method test yang sama untuk setiap input. Sepuluh cara penulisan email berarti sepuluh method yang identik kecuali satu barisnya. PHPUnit menyediakan data provider untuk kasus seperti ini: satu method test, banyak data set, dan setiap data set dihitung sebagai test tersendiri. Chapter ini membahas penulisan provider dengan attribute #[DataProvider], syarat static yang wajib dipatuhi, penamaan kasus agar kegagalan mudah dibaca, argumen berganda, cara menjalankan satu kasus saja dengan --filter, dan kapan sebaiknya test justru dipecah alih-alih diberi provider.
Test yang Disalin-Tempel #
Mari kita mulai dari penyakitnya. Menguji validasi email dengan tiga input valid akan tampak seperti ini tanpa provider (potongan dari sebuah class test):
<?php
declare(strict_types=1);
public function testMenerimaEmailStandar(): void
{
$this->assertNotFalse(filter_var("[email protected]", FILTER_VALIDATE_EMAIL));
}
public function testMenerimaEmailSubdomain(): void
{
$this->assertNotFalse(filter_var("[email protected]", FILTER_VALIDATE_EMAIL));
}
public function testMenerimaEmailAliasPlus(): void
{
$this->assertNotFalse(filter_var("[email protected]", FILTER_VALIDATE_EMAIL));
}Tiga method tersebut menempatkan satu-satunya perbedaan — string email — di tengah struktur yang berulang. Ketika aturan validasi berubah, Anda harus mengedit tiga method dengan pola yang sama; ketika kasus baru ditambahkan, satu method lagi disalin. Duplikasi seperti ini bukan hanya lambat ditulis, tetapi juga menyembunyikan bahwa semua kasus sesungguhnya menguji satu perilaku tunggal: fungsi validasi menerima email yang sah.
Attribute DataProvider dan Syarat Static #
Data provider membalik strukturnya: method test ditulis sekali dengan parameter, dan daftar kasus dipisah ke method penyedia data. Sejak PHPUnit 10, pengikatnya adalah attribute #[DataProvider], dan method penyedia wajib dideklarasikan static — PHPUnit memanggilnya tanpa membutuhkan instance test:
<?php
declare(strict_types=1);
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
final class ValidasiEmailTest extends TestCase
{
public static function penyediaEmailValid(): array
{
return [
"email standar" => ["[email protected]"],
"subdomain" => ["[email protected]"],
"alias tanda plus" => ["[email protected]"],
];
}
public static function penyediaEmailTidakValid(): array
{
return [
"string kosong" => [""],
"tanpa aksen at" => ["budi.example.com"],
"domain tanpa titik" => ["budi@example"],
];
}
#[DataProvider("penyediaEmailValid")]
public function testMenerimaEmailValid(string $email): void
{
$this->assertNotFalse(filter_var($email, FILTER_VALIDATE_EMAIL));
}
#[DataProvider("penyediaEmailTidakValid")]
public function testMenolakEmailTidakValid(string $email): void
{
$this->assertFalse(filter_var($email, FILTER_VALIDATE_EMAIL));
}
}Anatomi pola ini perlu dibaca teliti:
- Method penyedia (
penyediaEmailValid()) adalah methodpublic staticyang mengembalikan array; setiap elemennya adalah array berisi argumen untuk satu pemanggilan test. Elemen["[email protected]"]berarti test dijalankan dengan$email = "[email protected]". - Kunci string seperti
"email standar"menjadi nama kasus. Nama inilah yang tampil di output ketika suatu kasus gagal — kita akan melihat buktinya sebentar lagi. - Attribute
#[DataProvider("penyediaEmailValid")]di atas method test menunjuk nama method penyedia sebagai string. Attribute ini menggantikan anotasi@dataProvidergaya docblock lama yang sudah tidak dipakai lagi. - Parameter method test (
string $email) menerima isi setiap data set secara berurutan. Jumlah dan tipe parameter harus cocok dengan isi elemen penyedia. - Enam data set dari dua penyedia menghasilkan enam test tersendiri — bukan dua. PHPUnit melaporkan setiap kasus sebagai test individual dengan nama kasusnya masing-masing.
Argumen Berganda: Input dan Harapan #
Data set tidak terbatas satu argumen. Pola paling umum adalah pasangan input dan hasil yang diharapkan, seperti menguji diskon anggota 5% yang hanya aktif untuk belanja minimal Rp200.000:
<?php
declare(strict_types=1);
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
function hitungDiskonAnggota(int $totalBelanja): int
{
return $totalBelanja >= 200000 ? intdiv($totalBelanja * 5, 100) : 0;
}
final class DiskonAnggotaTest extends TestCase
{
public static function penyediaTotalBelanja(): array
{
return [
"di bawah batas minimum" => [100000, 0],
"tepat di batas minimum" => [200000, 10000],
"di atas batas minimum" => [350000, 17500],
"belanja besar" => [1250000, 62500],
];
}
#[DataProvider("penyediaTotalBelanja")]
public function testDiskonLimaPersenUntukAnggota(int $totalBelanja, int $diskonDiharapkan): void
{
$this->assertSame($diskonDiharapkan, hitungDiskonAnggota($totalBelanja));
}
}Setiap baris penyedia kini memuat dua nilai: [100000, 0] berarti hitungDiskonAnggota(100000) harus mengembalikan 0. Perhatikan bahwa kasus batas — “tepat di batas minimum” — mendapat barisnya sendiri; batas adalah tempat bug paling sering bersembunyi, dan provider membuat menambahkannya nyaris gratis. Dengan kedua berkas test di atas dalam direktori tests, seluruh suite berjalan dengan satu perintah:
php /home/z/my-project/bin/phpunit.phar testsBerikut output nyata dari perintah tersebut:
PHPUnit 12.5.39 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.23
.......... 10 / 10 (100%)
Time: 00:00.004, Memory: 28.05 MB
OK (10 tests, 10 assertions)Sepuluh titik mewakili enam kasus email ditambah empat kasus diskon — semuanya lahir dari dua method test saja. Bandingkan dengan sepuluh method salinan: jumlah perlindungan sama, biaya perawatannya jauh berbeda.
Nama Kasus dalam Output Kegagalan #
Nilai sesungguhnya dari kasus bernama baru terasa saat ada yang gagal. Bayangkan seorang developer salah menghitung ekspektasi untuk kasus “di atas batas minimum” — tertulis 17000, seharusnya 17500. Output kegagalannya:
<?php
declare(strict_types=1);
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
function hitungDiskonAnggota(int $totalBelanja): int
{
return $totalBelanja >= 200000 ? intdiv($totalBelanja * 5, 100) : 0;
}
final class DiskonSalahTest extends TestCase
{
public static function penyediaTotalBelanja(): array
{
return [
"di bawah batas minimum" => [100000, 0],
"tepat di batas minimum" => [200000, 10000],
"di atas batas minimum" => [350000, 17000],
];
}
#[DataProvider("penyediaTotalBelanja")]
public function testDiskonLimaPersenUntukAnggota(int $totalBelanja, int $diskonDiharapkan): void
{
$this->assertSame($diskonDiharapkan, hitungDiskonAnggota($totalBelanja));
}
}php /home/z/my-project/bin/phpunit.phar tests-failBerikut output nyata dari perintah tersebut:
PHPUnit 12.5.39 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.23
..F 3 / 3 (100%)
Time: 00:00.001, Memory: 28.05 MB
There was 1 failure:
1) DiskonSalahTest::testDiskonLimaPersenUntukAnggota@di atas batas minimum with data (350000, 17000)
Failed asserting that 17500 is identical to 17000.
/tmp/a97/providers/tests-fail/DiskonSalahTest.php:27
FAILURES!
Tests: 3, Assertions: 3, Failures: 1.Baris @di atas batas minimum with data (350000, 17000) langsung menunjuk kasus mana yang gagal beserta seluruh argumennya — tanpa membuka berkas, pembaca sudah tahu bahwa kombinasi 350000 dan ekspektasi 17000 bermasalah. Sekarang bandingkan dengan penyedia yang sama tanpa nama kasus:
1) DiskonNomorTest::testDiskonLimaPersenUntukAnggota#2 with data (350000, 17000)Tanpa nama, PHPUnit memberi nomor #2 dan Anda harus menghitung baris penyedia dari nol untuk tahu bahwa #2 adalah kasus “di atas batas minimum”. Nama kasus adalah dokumentasi gratis: tulis “email kosong”, “tepat di batas minimum”, atau “domain tanpa titik” — bukan #0, #1, #2.
Menjalankan Satu Kasus Saja #
Saat satu kasus gagal dan Anda sedang memperbaikinya, menjalankan seluruh suite hanya membuang waktu. Opsi --filter mempersempit eksekusi sampai ke satu data set. Untuk menjalankan hanya method testMenerimaEmailValid beserta ketiga kasusnya:
php /home/z/my-project/bin/phpunit.phar --filter 'testMenerimaEmailValid' testsBerikut output nyata dari perintah tersebut:
PHPUnit 12.5.39 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.23
... 3 / 3 (100%)
Time: 00:00.002, Memory: 28.05 MB
OK (3 tests, 3 assertions)Dan untuk menajamkan sampai satu kasus, sebutkan nama kasusnya:
php /home/z/my-project/bin/phpunit.phar --filter 'testMenolakEmailTidakValid with data set "string kosong"' testsBerikut output nyata dari perintah tersebut:
PHPUnit 12.5.39 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.23
. 1 / 1 (100%)
Time: 00:00.001, Memory: 28.05 MB
OK (1 test, 1 assertion)Filter bekerja sebagai pencocok pola terhadap nama test lengkap, sehingga --filter 'string kosong' saja juga cukup — nama kasusnya unik. Pola nama-method + nama-kasus adalah bentuk yang paling presisi dan layak dijadikan kebiasaan, terutama saat kasus bernama sama muncul di beberapa method test.
Kapan Sebaiknya Test Dipecah #
Data provider bukan obat untuk semua pengulangan. Aturan dasarnya: provider cocok ketika hanya data yang berbeda — setup, aksi, dan assertion tetap identik. Begitu struktur test harus bercabang untuk mengakomodasi sebuah kasus, cabang itu adalah test tersendiri yang menyamar:
| Situasi | Keputusan yang tepat |
|---|---|
| Hanya nilai input dan harapan berbeda | Satu method test + provider |
| Sebuah kasus butuh setup/fixture berbeda | Pecah menjadi test sendiri |
| Assertion berbeda untuk tiap kasus | Pecah — provider memaksa satu body test |
| Kasus spesial yang layak diceritakan namanya (mis. “tolak kartu kadaluarsa”) | Test sendiri dengan nama deskriptif |
| Test berjalan lambat dan kasus jarang gagal | Pertimbangkan pecah agar bisa dijalankan terpisah |
Penyebab pecahnya test paling sering: method test dengan provider berisi cabang if ($kasus === "...") — tanda bahwa data set sebenarnya bukan variasi data, melainkan variasi perilaku. Test semacam itu lebih jujur ditulis sebagai method terpisah dengan nama yang menjelaskan perilakunya, lalu provider hanya menampung variasi data yang benar-benar sejenis. Batas praktisnya juga perlu dihormati: penyedia puluhan kasus membuat satu kegagalan sulit dibedakan dari kasus lainnya — pertimbangkan memecah penyedia besar menjadi beberapa penyedia bertema (valid, tidak valid, batas).
Ringkasan #
Data provider mengubah duplikasi test menjadi tabel kasus: satu method test, method penyedia static bertanda #[DataProvider], dan argumen yang mengalir per kasus. Nama kasus menjelaskan dirinya di output kegagalan (@tepat di batas minimum alih-alih #1), argumen berganda memuat pasangan input-harapan, dan --filter menajamkan eksekusi sampai satu data set. Disiplinnya ada di meja pemisahan: begitu setup atau assertion berbeda, pecah test. Dengan assertion dan provider, logika murni sudah teruji rapi — tetapi method test kita selama ini masih memanggil dependensi nyata seperti database dan SMTP. Mengganti dependensi itu dengan padanan yang terkendali adalah pekerjaan chapter berikutnya tentang Test Doubles
.