Menjaga kualitas kode dengan PHPUnit: membedakan unit vs integration test, WebTestCase untuk memeriksa response HTTP, fixtures untuk data test yang deterministik, MockHttpClient dari episode 11, serta Panther untuk browser testing end-to-end.

Di episode 16 aplikasi bisa berjalan di tiga environment. Episode 17 menambahkan jaring pengaman yang membuat perubahan-perubahan itu berani dilakukan: testing. Kode tanpa test adalah janji tanpa bukti — dan di tim profesional, "sudah dicoba manual" bukan jawaban yang diterima.
Mengapa testing penting di series Symfony? Karena fitur-fitur yang kita bangun (form, security, API) saling bergantung. Satu perubahan di entity bisa merusak API dan form sekaligus. Test otomatis menangkap ini dalam hitungan detik, sebelum merusak production.
Install PHPUnit dan WebTestCase:
composer require --dev symfony/test-packHasilnya: phpunit.xml.dist, folder tests/, dan browser-kit + phpunit-bridge. Struktur standar:
tests/
├── Controller/ # WebTestCase — test HTTP penuh
├── Service/ # unit test service murni
├── Entity/ # unit test entity
└── TestKernel.php # kernel minimal untuk testJalankan:
php bin/phpunitUnit test menguji satu class dalam isolasi, tanpa framework, tanpa database. Contoh menguji service murni:
namespace App\Tests\Service;
use App\Service\Slugger;
use PHPUnit\Framework\TestCase;
final class SluggerTest extends TestCase
{
public function testSlug(): void
{
$slugger = new Slugger();
self::assertSame('belajar-symfony', $slugger->slugify('Belajar Symfony'));
}
public function testEmptyInput(): void
{
$slugger = new Slugger();
self::assertSame('', $slugger->slugify(''));
}
}Unit test cepat (tidak menyentuh IO) — karena itu bisa dijalankan ribuan kali per commit. Service dengan dependency di-mock atau diinject fake sederhana.
Integration test paling berharga: memeriksa response lengkap — status, redirect, isi HTML, dan database.
namespace App\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
final class ArticleControllerTest extends WebTestCase
{
public function testIndexShowsArticles(): void
{
$client = static::createClient();
$crawler = $client->request('GET', '/articles');
self::assertResponseIsSuccessful();
self::assertSelectorTextContains('h1', 'Daftar Artikel');
}
public function testUnknownArticleReturns404(): void
{
$client = static::createClient();
$client->request('GET', '/articles/9999');
self::assertResponseStatusCodeSame(404);
}
}Assertion yang paling sering dipakai:
| Assertion | Fungsi |
|---|---|
assertResponseIsSuccessful() | Status 2xx |
assertResponseRedirects('/login') | Redirect terjadi |
assertSelectorTextContains('h1', '...') | Isi HTML |
assertResponseStatusCodeSame(403) | Status tertentu |
assertEmailCount(1) | Email terkirim (Mailer test) |
createClient() memakai test kernel dan APP_ENV=test — database test terisolasi, session bersih setiap test.
WebTestCase default menggunakan database test yang sama — test yang mengubah data bisa mengganggu test lain. Pola sehat:
doctrine:fixtures:load --env=test sebelum suite (lihat phpunit.xml.dist bootstrap).ResetDatabase attribute untuk test yang menulis data:use Doctrine\Bundle\FixturesBundle\Attribute\ResetDatabase;
final class ArticleControllerTest extends WebTestCase
{
#[ResetDatabase]
public function testCreateArticle(): void
{
$client = static::createClient();
$client->request('POST', '/articles/new', [
'article' => ['title' => 'Judul Baru', 'body' => 'Konten...'],
]);
self::assertResponseRedirects('/articles');
}
}#[ResetDatabase] me-reset database sebelum tiap test — deterministik, tidak saling menggantung.
Alur login-test yang umum:
public function testAdminDashboardRequiresLogin(): void
{
$client = static::createClient();
$client->request('GET', '/admin/dashboard');
self::assertResponseRedirects('/login');
}
public function testLoginWorks(): void
{
$client = static::createClient();
$crawler = $client->request('GET', '/login');
$form = $crawler->selectButton('Masuk')->form();
$client->submit($form, [
'email' => 'admin@example.com',
'password' => 'rahasia',
]);
self::assertResponseRedirects('/dashboard');
$client->followRedirect();
self::assertSelectorTextContains('h1', 'Dashboard');
}Perhatikan: user test dibuat di fixtures (admin@example.com), jadi test tidak bergantung pada data production.
Di episode 11 kita membangun MockHttpClient. Gabungkan dengan test sekarang:
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
public function testWeather(): void
{
$mock = new MockHttpClient([
new MockResponse('{"temperature":29.5}', ['http_code' => 200]),
]);
$service = new WeatherService($mock);
$result = $service->current('Jakarta');
self::assertSame(29.5, $result['temperature']);
}Ini membuat test cepat dan deterministik — tidak ada jaringan, tidak ada API eksternal yang tiba-tiba down.
Untuk alur JavaScript (modal, SPA, UX Live Components), gunakan Panther — WebDriver yang memakai browser sungguhan:
composer require --dev symfony/pantheruse Symfony\Component\Panther\PantherTestCase;
final class LiveComponentTest extends PantherTestCase
{
public function testCounterIncrements(): void
{
$client = self::createPantherClient();
$client->request('GET', '/counter');
$client->clickLink('Tambah');
self::assertSelectorTextContains('.counter', '1');
}
}Panther memakai ChromeDriver (atau Selenium). Untuk alur yang mengandalkan JavaScript, inilah satu-satunya cara — panther di CI bisa berjalan dalam container (episode 23).
Tip
Tumpukan test yang sehat: banyak unit test (cepat) + secukupnya WebTestCase (integrasi) + sedikit Panther (E2E). Jangan menulis 500 test Panther — suite akan lambat dan rapuh. Prioritas: logika bisnis paling banyak di-test, bukan jalan halaman yang jarang berubah.
Untuk melihat bagian kode yang belum di-test:
XDEBUG_MODE=coverage php bin/phpunit --coverage-textFokus pada tren, bukan angka absolut: 70% coverage yang relevan (logika bisnis) lebih baik daripada 95% yang hanya menguji getter/setter.
Pada episode 17 ini, kalian telah membangun jaring pengaman kualitas.
Inti yang harus dibawa pulang:
assertResponseIsSuccessful(), assertSelectorTextContains(), dan kawan-kawan.#[ResetDatabase] membuat data test deterministik.MockHttpClient untuk service yang memanggil API.Di episode 18 selanjutnya kita memperketat pertahanan: Security Headers & OWASP Best Practices — CSP, HSTS, X-Frame-Options, proteksi CSRF/XSS/SQLi, dan keamanan file upload. Sampai jumpa di episode 18!