Mengamankan kualitas aplikasi dengan Codeception: menyiapkan framework testing untuk Yii, menulis unit test untuk logika model, functional test untuk alur request-response, acceptance test untuk skenario browser end-to-end, serta memakai fixtures untuk data test yang terisolasi dan deterministik.

Sejauh ini kita membangun fitur dengan cepat. Tapi kecepatan tanpa jaring pengaman adalah utang: satu perubahan kecil di model Post bisa merusak halaman index, dan kalian baru tahu setelah production error. Testing adalah jaring pengaman itu — dan di ekosistem Yii, tool standarnya adalah Codeception.
Codeception menawarkan empat lapis pengujian yang saling melengkapi: unit (logika murni), functional (alur request internal), acceptance (lewat browser sungguhan), dan fixtures (data test deterministik). Di episode ini kita menyiapkan semuanya, menulis test untuk CRUD dan API yang kita bangun di episode 5 dan 10, lalu menjalankannya sebagai bagian rutin pengembangan.
Codeception adalah framework testing PHP dengan konsep suites — setiap suite adalah lingkungan test dengan modul pendukung berbeda. Template Yii2 basic sudah menyertakan Codeception; jika belum, install:
composer require codeception/codeception --dev
composer require yiisoft/yii2-codeception --devTemplate basic sudah menyediakan struktur tests/:
tests/
├── _support/
│ ├── FunctionalTester.php
│ ├── UnitTester.php
│ └── AcceptanceTester.php
├── unit/ # unit test (model, helper)
├── functional/ # functional test (request-response)
├── acceptance/ # acceptance test (browser)
├── fixtures/ # data test
├── codeception.yml
└── unit.suite.yml, functional.suite.yml, acceptance.suite.ymlcodeception.yml mengonfigurasi keseluruhan, termasuk koneksi database test terpisah:
actor: Tester
paths:
tests: tests
output: tests/_output
support: tests/_support
data: tests
settings:
bootstrap: _bootstrap.php
colors: true
memory_limit: 1024M
params:
- env.php
modules:
config:
Yii2:
configFile: '../config/test.php'
cleanup: trueKunci penting: configFile: '../config/test.php' — file konfigurasi yang memakai database test terpisah (misal SQLite atau DB khusus test), agar test tidak menyentuh data pengembangan.
Unit test memverifikasi logika murni — validasi, relasi, method model — tanpa HTTP. Contoh menguji Post:
<?php
namespace tests\unit\models;
use app\models\Post;
use Codeception\Test\Unit;
use tests\fixtures\PostFixture;
class PostTest extends Unit
{
public function _fixtures(): array
{
return [
'posts' => PostFixture::class,
];
}
public function testRulesRequireTitle(): void
{
$post = new Post();
$post->body = 'Konten';
$post->status = 'draft';
$this->assertFalse($post->validate());
$this->assertArrayHasKey('title', $post->getErrors());
}
public function testSlugMustBeUnique(): void
{
$post = new Post([
'title' => 'Post duplikat',
'slug' => 'post-pertama', // sudah dipakai fixture
'status' => 'draft',
]);
$this->assertFalse($post->validate());
$this->assertArrayHasKey('slug', $post->getErrors());
}
public function testValidPostPasses(): void
{
$post = new Post([
'title' => 'Post valid',
'slug' => 'post-valid',
'body' => 'Konten lengkap',
'status' => 'published',
]);
$this->assertTrue($post->validate());
}
}vendor/bin/codecept run unitHasil: testRulesRequireTitle memverifikasi title wajib, testSlugMustBeUnique memverifikasi validasi unique terhadap fixture, dan testValidPostPasses memastikan data benar lolos. Test ini deterministik — jalan kapan pun, hasil sama.
Functional test menjalankan aplikasi Yii di dalam memori — request dibuat, controller dipanggil, response diperiksa, tanpa browser sungguhan:
<?php
namespace tests\functional;
use app\models\User;
use FunctionalTester;
class PostCreateCest
{
public function testCreateRequiresLogin(FunctionalTester $I): void
{
$I->amOnPage('/post/create');
$I->see('Login', 'h1');
}
public function testCreatePostAsLoggedInUser(FunctionalTester $I): void
{
$I->amLoggedInAs(User::findByUsername('admin'));
$I->amOnPage('/post/create');
$I->fillField('#post-title', 'Post dari functional test');
$I->fillField('#post-body', 'Konten test');
$I->click('Simpan');
$I->see('Post disimpan.');
$I->seeRecord('app\models\Post', ['title' => 'Post dari functional test']);
}
}vendor/bin/codecept run functionalYang diuji bukan sekadar "halaman bisa dibuka", melainkan perilaku: user guest ditolak akses create, user login bisa membuat post, dan post benar-benar tersimpan di database (seeRecord). Inilah tes yang paling banyak memberikan nilai per jam penulisannya — ringan, cepat, dan mencakup alur MVC lengkap.
Acceptance test menggunakan browser sungguhan (via WebDriver) — menguji hal yang tidak bisa ditangkap functional test: JavaScript, CSS, klik nyata:
<?php
namespace tests\acceptance;
use AcceptanceTester;
class PostCrudCest
{
public function testCrudFlow(AcceptanceTester $I): void
{
$I->amOnPage('/post');
$I->see('Post Blog', 'h1');
$I->click('Buat Post');
$I->fillField('#post-title', 'Post dari acceptance test');
$I->fillField('#post-body', 'Konten acceptance');
$I->click('Simpan');
$I->see('Post disimpan.');
$I->see('Post dari acceptance test');
}
}vendor/bin/codecept run acceptanceAcceptance test butuh WebDriver (ChromeDriver/Selenium) berjalan. Karena lebih berat, jadikan acceptance test sebagai lapisan terakhir yang menangkap skenario paling kritis — bukan untuk setiap perubahan kecil.
Tip
Strategi proporsi yang sehat: banyak unit test (murah, cepat, mencakup logika), functional test untuk setiap alur penting (login, CRUD, API), dan acceptance test hanya untuk skenario end-to-end kritis. Banyak tim membuat kesalahan dengan menulis acceptance test untuk segalanya, lalu test suite jadi lambat dan jarang dijalankan.
Fixtures adalah data yang disediakan untuk test — deterministik dan bersih setiap kali test dijalankan. Contoh fixture untuk Post:
<?php
namespace tests\fixtures;
use yii\test\ActiveFixture;
class PostFixture extends ActiveFixture
{
public $modelClass = 'app\models\Post';
public $depends = [
'tests\fixtures\UserFixture',
];
}<?php
return [
'post1' => [
'title' => 'Post Pertama',
'slug' => 'post-pertama',
'body' => 'Konten fixture pertama',
'status' => 'published',
'user_id' => 1,
'created_at' => 1710000000,
'updated_at' => 1710000000,
],
'post2' => [
'title' => 'Post Kedua',
'slug' => 'post-kedua',
'body' => 'Konten fixture kedua',
'status' => 'draft',
'user_id' => 1,
'created_at' => 1710003600,
'updated_at' => 1710003600,
],
];Setiap test yang mendeklarasikan _fixtures() mendapat data bersih yang di-reload otomatis sebelum test berjalan. Perhatikan depends — PostFixture membutuhkan UserFixture terlebih dahulu agar foreign key valid. Fixtures juga bisa dipakai di luar test, misal untuk seeding data pengembangan (episode 6).
API yang kita bangun di episode 10 juga bisa ditest secara functional — baik lewat module Yii2 maupun HTTP API:
<?php
namespace tests\functional\api;
use app\models\User;
use FunctionalTester;
class PostApiCest
{
public function testIndexRequiresToken(FunctionalTester $I): void
{
$I->sendGET('/api/posts');
$I->seeResponseCodeIs(401);
}
public function testIndexWithToken(FunctionalTester $I): void
{
$user = User::findByUsername('admin');
$token = $user->access_token;
$I->amHttpAuthenticated('', $token);
$I->sendGET('/api/posts');
$I->seeResponseCodeIs(200);
$I->seeResponseIsJson();
$I->seeResponseContainsJson(['title' => 'Post Pertama']);
}
}vendor/bin/codecept runDua hal yang dijamin: API menolak tanpa token (401), dan dengan token benar mengembalikan data JSON yang sesuai — termasuk perilaku yang sering kena regresi saat serializer atau autentikasi berubah.
Test yang tidak dijalankan otomatis akan terlupakan. Paling tidak, jalankan test di CI pipeline setiap push — kita detailkan di episode 22. Untuk kebiasaan lokal, buat script di composer.json:
{
"scripts": {
"test": "vendor/bin/codecept run",
"test:unit": "vendor/bin/codecept run unit"
}
}composer testDengan satu perintah, seluruh suite berjalan — dan perintah yang sama dipakai CI.
Inti yang harus dibawa pulang:
configFile: config/test.php mengisolasi test ke database test terpisah.ActiveFixture + depends menyediakan data bersih per test.composer test dan CI.Di episode 17 selanjutnya, kita mengeraskan aplikasi: security best practices — menangkal XSS dengan Html::encode, CSRF token, SQL injection lewat parameter binding, password hashing yang benar, serta security headers untuk production. Inilah episode yang menentukan apakah aplikasi kalian bertahan di internet sungguhan. Sampai jumpa di episode 17!