Membangun kepercayaan pada kode dengan PHPUnit: setup test environment, menulis unit test dan feature test, menguji interaksi database, serta praktik mengetes controller dan model secara nyata.

Sejauh ini kalian menguji aplikasi dengan membuka browser dan klik manual. Masalahnya: manual testing tidak konsisten, melelahkan, dan mudah melewatkan regresi. Di episode 17 ini kita masuk ke disiplin yang mengubah cara kerja kalian: automated testing dengan PHPUnit.
Mengapa penting? Karena aplikasi akan terus berubah — menambah fitur, refactor, upgrade library — dan setiap perubahan berpotensi memecahkan yang sudah jalan. Test adalah jaring pengaman yang menjerit sebelum regresi sampai ke produksi. CodeIgniter 4 datang dengan integrasi PHPUnit yang siap pakai.
Template appstarter sudah menyertakan phpunit.xml.dist dan folder tests/. Pastikan dependency test terinstall:
composer require --dev phpunit/phpunitCek file phpunit.xml.dist — ia mendefinisikan environment testing, termasuk database test:
<phpunit bootstrap="vendor/codeigniter4/framework/system/Test/bootstrap.php">
<testsuites>
<testsuite name="app">
<directory>./tests</directory>
</testsuite>
</testsuites>
<php>
<const name="HOMEPATH" value="./"/>
<const name="CONFIGPATH" value="./app/Config/"/>
<const name="PUBLICPATH" value="./public/"/>
<env name="CI_ENVIRONMENT" value="testing"/>
<env name="database.tests.default.hostname" value="localhost"/>
<env name="database.tests.default.database" value="ci_blog_test"/>
</php>
</phpunit>Perhatikan CI_ENVIRONMENT = testing dan database group tests yang terpisah — test tidak boleh menimpa database development/produksi kalian.
php spark testphp spark test adalah wrapper CodeIgniter untuk PHPUnit dengan konfigurasi yang sudah dimuat.
Warning
Jangan pernah menjalankan test terhadap database produksi. Sediakan database khusus test (misal ci_blog_test) dan set via database.tests.* di phpunit.xml.dist — test akan membuat/menghapus data tanpa ampun.
Unit test menguji satu unit kode secara terisolasi — biasanya method class. Contoh: test library Slugger dari episode 10.
<?php
namespace Tests\Unit;
use App\Libraries\Slugger;
use CodeIgniter\Test\CIUnitTestCase;
final class SluggerTest extends CIUnitTestCase
{
public function testMakeMengubahJudulMenjadiSlug(): void
{
$slugger = new Slugger();
$this->assertSame(
'belajar-codeigniter-4',
$slugger->make('Belajar CodeIgniter 4')
);
}
public function testMakeMenghilangkanSpasiGanda(): void
{
$slugger = new Slugger();
$this->assertSame('hello-world', $slugger->make('Hello World'));
}
}Class test extends CIUnitTestCase (subclass PHPUnit\Framework\TestCase). assertSame() mengecek kesamaan nilai dan tipe — pilihan yang lebih ketat daripada assertEquals().
Feature test menjalankan request ke aplikasi dan mengecek respons — menguji controller, route, dan view sekaligus.
<?php
namespace Tests\Feature;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\FeatureTestTrait;
final class PostPageTest extends CIUnitTestCase
{
use FeatureTestTrait;
public function testHomePageMenampilkanJudul(): void
{
$result = $this->call('GET', '/');
$result->assertStatus(200);
$result->assertSee('Selamat Datang');
}
public function testPostYangTidakAdaMengembalikan404(): void
{
$result = $this->call('GET', '/posts/99999');
$result->assertStatus(404);
}
public function testDashboardMenolakTanpaLogin(): void
{
$result = $this->call('GET', '/dashboard');
$result->assertStatus(302); // redirect ke login
$result->assertRedirectTo('/login');
}
}FeatureTestTrait memberi method call($method, $path) dan assertion: assertStatus(), assertSee(), assertRedirectTo(). Test ini mensimulasikan HTTP request tanpa menjalankan server — cepat dan deterministik.
Tip
Feature test adalah lapisan pertama proteksi: jika route berubah, response berubah, atau view error — test langsung gagal. Jangan hanya menulis test untuk skenario sukses; uji juga jalur gagal (404, redirect, validation error).
Untuk menguji model dengan database nyata, CodeIgniter menyediakan DatabaseTestTrait yang menjalankan migration dan seed otomatis per test.
<?php
namespace Tests\Feature;
use App\Models\PostModel;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
final class PostModelTest extends CIUnitTestCase
{
use DatabaseTestTrait;
protected $refresh = true;
protected $namespace = 'App';
public function testModelBisaMenyimpanPost(): void
{
$model = new PostModel();
$id = $model->insert([
'title' => 'Test Post',
'slug' => 'test-post',
'body' => 'Isi test',
'status' => 'draft',
]);
$this->assertIsInt($id);
$this->seeInDatabase('posts', ['title' => 'Test Post']);
}
public function testModelMenolakSlugDuplikat(): void
{
$model = new PostModel();
$model->insert([
'title' => 'Satu',
'slug' => 'satu-satu',
'body' => 'x',
'status' => 'draft',
]);
$ok = $model->insert([
'title' => 'Dua',
'slug' => 'satu-satu',
'body' => 'y',
'status' => 'draft',
]);
$this->assertFalse($ok);
$this->assertNotEmpty($model->errors());
}
}$refresh = true membuat migration dan seed berjalan sebelum setiap test, lalu database di-reset setelahnya — setiap test memulai dari kondisi bersih. seeInDatabase() mengecek data benar-benar tersimpan.
Warning
Pastikan database test terpisah dari development. Test model menghapus dan membuat ulang tabel terus-menerus ($refresh = true) — menjalankannya di database ci_blog (development) akan menghapus data kerja kalian.
Controller bisa dites lewat feature test, atau langsung menguji method-nya. Untuk response JSON (episode 13), gunakan assertJSONFragment():
public function testApiListMengembalikanJSON(): void
{
$result = $this->call('GET', '/api/v1/posts');
$result->assertStatus(200);
$result->assertHeader('Content-Type', 'application/json');
$result->assertJSONFragment(['status' => 'published']);
}Menguji controller lewat HTTP layer lebih realistis daripada memanggil method langsung — karena mencakup routing, filter, dan response handling sekaligus.
composer require --dev phpunit/phpunit, jalankan php spark test.database.tests.* — jangan sentuh DB development.CIUnitTestCase): uji method class secara terisolasi.FeatureTestTrait): $this->call(), assertStatus, assertSee, assertRedirectTo.DatabaseTestTrait): migration + seed per test, seeInDatabase().Inti yang harus dibawa pulang:
Di episode 18 selanjutnya kita akan membahas security hardening (OWASP) — meninjau CSRF, XSS, SQL injection, keamanan file upload, dan security headers, lalu melakukan audit dan patch pada aplikasi kita. Ini episode paling "serius" di series ini!