Belajar Symfony - Testing (PHPUnit)
Episode 17 of 27

Belajar Symfony - Testing (PHPUnit)

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.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

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.

Setup dan Struktur Test

Install PHPUnit dan WebTestCase:

Install test dependencies
composer require --dev symfony/test-pack

Hasilnya: phpunit.xml.dist, folder tests/, dan browser-kit + phpunit-bridge. Struktur standar:

Struktur folder test
tests/
├── Controller/       # WebTestCase — test HTTP penuh
├── Service/          # unit test service murni
├── Entity/           # unit test entity
└── TestKernel.php    # kernel minimal untuk test

Jalankan:

Jalankan semua test
php bin/phpunit

Unit Test: Menguji Isolasi

Unit test menguji satu class dalam isolasi, tanpa framework, tanpa database. Contoh menguji service murni:

tests/Service/SluggerTest.php
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.

WebTestCase: Menguji HTTP Penuh

Integration test paling berharga: memeriksa response lengkap — status, redirect, isi HTML, dan database.

tests/Controller/ArticleControllerTest.php
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:

AssertionFungsi
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.

Data Test: Fixtures dan Isolasi Database

WebTestCase default menggunakan database test yang sama — test yang mengubah data bisa mengganggu test lain. Pola sehat:

  1. doctrine:fixtures:load --env=test sebelum suite (lihat phpunit.xml.dist bootstrap).
  2. ResetDatabase attribute untuk test yang menulis data:
Test dengan reset database
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.

Testing Form dan Security Flow

Alur login-test yang umum:

Test alur login
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.

Testing HTTP Client: Mock Tanpa Jaringan

Di episode 11 kita membangun MockHttpClient. Gabungkan dengan test sekarang:

Test service dengan MockHttpClient
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.

Panther: Browser Testing

Untuk alur JavaScript (modal, SPA, UX Live Components), gunakan Panther — WebDriver yang memakai browser sungguhan:

Install Panther
composer require --dev symfony/panther
Test interaksi dengan Panther
use 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.

Code Coverage

Untuk melihat bagian kode yang belum di-test:

Generate coverage
XDEBUG_MODE=coverage php bin/phpunit --coverage-text

Fokus pada tren, bukan angka absolut: 70% coverage yang relevan (logika bisnis) lebih baik daripada 95% yang hanya menguji getter/setter.

Penutup

Pada episode 17 ini, kalian telah membangun jaring pengaman kualitas.

Inti yang harus dibawa pulang:

  • Unit test = isolasi class; WebTestCase = response HTTP penuh.
  • assertResponseIsSuccessful(), assertSelectorTextContains(), dan kawan-kawan.
  • Fixtures + #[ResetDatabase] membuat data test deterministik.
  • Alur login & form di-test seperti user sungguhan.
  • MockHttpClient untuk service yang memanggil API.
  • Panther untuk alur JavaScript; coverage untuk mengukur cakupan.

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!

Belajar Symfony - Testing (PHPUnit) | Belajar Symfony