Pengujian Symfony di 2026: PHPUnit, KernelTestCase, dan Functional Tests
Panduan lengkap pengujian aplikasi Symfony menggunakan PHPUnit 12, KernelTestCase untuk integration testing, dan WebTestCase untuk functional tests dengan praktik terbaik.

Pengujian Symfony dengan PHPUnit 12 menyediakan pendekatan terstruktur untuk memvalidasi perilaku aplikasi mulai dari unit test hingga simulasi HTTP request lengkap. Infrastruktur testing framework ini terintegrasi erat dengan service container, sehingga memudahkan pengujian service, controller, dan operasi database dengan boilerplate minimal.
Symfony 7.3 hadir dengan dukungan PHPUnit 12, peningkatan isolasi test melalui ResetInterface, dan komponen CrawlerSelector untuk assertion functional test yang lebih mudah dibaca. WebTestCase kini mendukung assertion HTTP/2 push secara bawaan.
Unit Testing Services dengan PHPUnit 12
Unit test memverifikasi class individual secara terisolasi. Untuk service Symfony yang tidak memiliki dependensi pada container, test PHPUnit standar dapat berjalan tanpa keterlibatan framework.
namespace App\Tests\Unit\Service;
use App\Service\PriceCalculator;
use PHPUnit\Framework\TestCase;
final class PriceCalculatorTest extends TestCase
{
private PriceCalculator $calculator;
protected function setUp(): void
{
// Create instance directly - no container needed
$this->calculator = new PriceCalculator(vatRate: 0.20);
}
public function testCalculateTotalWithVat(): void
{
$result = $this->calculator->calculateTotal(100.00);
// Assert exact decimal value with delta for float comparison
$this->assertEqualsWithDelta(120.00, $result, 0.001);
}
public function testCalculateTotalWithZeroAmount(): void
{
$result = $this->calculator->calculateTotal(0.00);
$this->assertEqualsWithDelta(0.00, $result, 0.001);
}
}Unit test murni berjalan lebih cepat dibandingkan integration test dan dapat menunjukkan kegagalan dengan tepat. Gunakan unit test untuk class logika bisnis yang mentransformasi data atau melakukan perhitungan.
Integration Testing dengan KernelTestCase
Ketika service bergantung pada container Symfony—koneksi database, cache adapter, atau service lainnya—KernelTestCase mem-boot kernel minimal dan menyediakan akses ke service container.
namespace App\Tests\Integration\Repository;
use App\Entity\User;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
final class UserRepositoryTest extends KernelTestCase
{
private EntityManagerInterface $em;
private UserRepository $repository;
protected function setUp(): void
{
// Boot kernel and fetch services from container
self::bootKernel();
$container = static::getContainer();
$this->em = $container->get(EntityManagerInterface::class);
$this->repository = $container->get(UserRepository::class);
}
public function testFindByEmailReturnsUserWhenExists(): void
{
// Arrange: create and persist test user
$user = new User();
$user->setEmail('test@example.com');
$user->setPassword('hashed_password');
$this->em->persist($user);
$this->em->flush();
// Act: query through repository
$found = $this->repository->findByEmail('test@example.com');
// Assert: verify retrieval
$this->assertNotNull($found);
$this->assertSame('test@example.com', $found->getEmail());
}
protected function tearDown(): void
{
// Clean up to prevent test pollution
$this->em->createQuery('DELETE FROM App\Entity\User')->execute();
parent::tearDown();
}
}Method getContainer() mengembalikan container test yang mengekspos service private untuk keperluan pengujian. Hal ini berbeda dengan runtime container di mana service private tetap tidak dapat diakses.
Isolasi Database dengan Transaksi
Isolasi test mencegah data dari satu test mempengaruhi test lainnya. DAMADoctrineTestBundle membungkus setiap test dalam transaksi yang di-rollback secara otomatis.
namespace App\Tests\Integration\Service;
use App\Entity\Order;
use App\Service\OrderService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
final class OrderServiceTest extends KernelTestCase
{
private OrderService $orderService;
private EntityManagerInterface $em;
protected function setUp(): void
{
self::bootKernel();
$container = static::getContainer();
$this->orderService = $container->get(OrderService::class);
$this->em = $container->get(EntityManagerInterface::class);
}
public function testCreateOrderPersistsToDatabase(): void
{
// With DAMA bundle, this persists within a transaction
$order = $this->orderService->create(
customerId: 1,
items: [['sku' => 'ABC123', 'quantity' => 2]]
);
// Verify persistence
$this->em->clear();
$persisted = $this->em->find(Order::class, $order->getId());
$this->assertNotNull($persisted);
$this->assertCount(1, $persisted->getItems());
}
// Transaction rolls back after test - no cleanup needed
}Konfigurasi bundle dalam phpunit.xml.dist dengan menambahkan extension. Setiap test kemudian berjalan dalam isolasi lengkap tanpa kode cleanup manual.
Siap menguasai wawancara Symfony Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
Functional Testing dengan WebTestCase
Functional test mensimulasikan HTTP request dan memverifikasi response. WebTestCase menyediakan client yang membuat request ke controller tanpa memulai web server.
namespace App\Tests\Functional\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Response;
final class ProductControllerTest extends WebTestCase
{
public function testListProductsReturnsJsonArray(): void
{
// Create client and make GET request
$client = static::createClient();
$client->request('GET', '/api/products');
// Assert response status
$this->assertResponseIsSuccessful();
$this->assertResponseStatusCodeSame(Response::HTTP_OK);
// Assert JSON structure
$this->assertResponseHeaderSame('Content-Type', 'application/json');
$data = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($data);
$this->assertArrayHasKey('products', $data);
}
public function testCreateProductRequiresAuthentication(): void
{
$client = static::createClient();
$client->request('POST', '/api/products', [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode(['name' => 'New Product', 'price' => 29.99]));
// Unauthenticated requests should return 401
$this->assertResponseStatusCodeSame(Response::HTTP_UNAUTHORIZED);
}
}Method $client->request() menerima HTTP method, URI, parameter, file, variabel server, dan konten body. Symfony memproses request melalui kernel lengkap, termasuk middleware, security, dan routing.
Pengujian Request Terotentikasi
Untuk endpoint yang membutuhkan autentikasi, komponen security Symfony menyediakan method loginUser() pada test client.
namespace App\Tests\Functional\Controller;
use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
final class AdminControllerTest extends WebTestCase
{
public function testAdminDashboardRequiresAdminRole(): void
{
$client = static::createClient();
$container = static::getContainer();
// Fetch a test user with ROLE_ADMIN
$userRepository = $container->get(UserRepository::class);
$adminUser = $userRepository->findOneBy(['email' => 'admin@example.com']);
// Authenticate the client
$client->loginUser($adminUser);
// Access protected route
$client->request('GET', '/admin/dashboard');
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('h1', 'Admin Dashboard');
}
public function testRegularUserCannotAccessAdmin(): void
{
$client = static::createClient();
$container = static::getContainer();
$userRepository = $container->get(UserRepository::class);
$regularUser = $userRepository->findOneBy(['email' => 'user@example.com']);
$client->loginUser($regularUser);
$client->request('GET', '/admin/dashboard');
// Should be forbidden, not redirected to login
$this->assertResponseStatusCodeSame(403);
}
}Method loginUser() mengatur security token tanpa melalui form login, membuat test lebih cepat dan lebih fokus.
Pengujian Form dan Penanganan CSRF
Pengujian form memerlukan penanganan token CSRF. Komponen crawler mengekstrak elemen form dan mengirimkannya dengan penanganan token yang tepat.
namespace App\Tests\Functional\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
final class RegistrationControllerTest extends WebTestCase
{
public function testRegistrationFormSubmission(): void
{
$client = static::createClient();
// Load the registration page
$crawler = $client->request('GET', '/register');
// Extract the form and fill fields
$form = $crawler->selectButton('Register')->form([
'registration_form[email]' => 'newuser@example.com',
'registration_form[plainPassword]' => 'SecurePass123!',
'registration_form[agreeTerms]' => true,
]);
// Submit - CSRF token is included automatically
$client->submit($form);
// Assert redirect after successful registration
$this->assertResponseRedirects('/login');
// Follow redirect and verify flash message
$client->followRedirect();
$this->assertSelectorTextContains('.alert-success', 'Registration successful');
}
public function testRegistrationValidationErrors(): void
{
$client = static::createClient();
$crawler = $client->request('GET', '/register');
// Submit with invalid email
$form = $crawler->selectButton('Register')->form([
'registration_form[email]' => 'not-an-email',
'registration_form[plainPassword]' => '123', // Too short
'registration_form[agreeTerms]' => false,
]);
$client->submit($form);
// Should stay on same page with errors
$this->assertResponseIsSuccessful();
$this->assertSelectorExists('.invalid-feedback');
}
}Method selectButton() menemukan form berdasarkan teks tombol submit-nya. Untuk form tanpa tombol yang terlihat, gunakan selectForm() dengan CSS selector.
Pengujian Console Commands
Command sering melakukan operasi kritis seperti import data atau scheduled jobs. CommandTester mengeksekusi command dan menangkap output-nya.
namespace App\Tests\Command;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;
final class ImportUsersCommandTest extends KernelTestCase
{
public function testImportUsersWithValidCsv(): void
{
self::bootKernel();
$application = new Application(self::$kernel);
$command = $application->find('app:import-users');
$tester = new CommandTester($command);
// Execute with arguments and options
$tester->execute([
'file' => 'tests/fixtures/users.csv',
'--dry-run' => true,
]);
// Assert exit code (0 = success)
$this->assertSame(0, $tester->getStatusCode());
// Assert output contains expected text
$output = $tester->getDisplay();
$this->assertStringContainsString('Imported 5 users', $output);
}
public function testImportUsersFailsWithMissingFile(): void
{
self::bootKernel();
$application = new Application(self::$kernel);
$command = $application->find('app:import-users');
$tester = new CommandTester($command);
$tester->execute(['file' => 'nonexistent.csv']);
// Non-zero exit code indicates failure
$this->assertSame(1, $tester->getStatusCode());
$this->assertStringContainsString('File not found', $tester->getDisplay());
}
}Untuk command dengan input interaktif, berikan ['interactive' => false] sebagai argumen kedua ke execute(), atau gunakan setInputs() untuk mensimulasikan respons pengguna.
Pengujian Messenger untuk Async Jobs
Pengujian handler Symfony Messenger memerlukan verifikasi bahwa message di-dispatch dengan benar dan handler memprosesnya sesuai harapan.
namespace App\Tests\Integration\Message;
use App\Message\SendWelcomeEmail;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Transport\InMemoryTransport;
final class SendWelcomeEmailHandlerTest extends KernelTestCase
{
public function testWelcomeEmailIsDispatched(): void
{
self::bootKernel();
$container = static::getContainer();
$bus = $container->get(MessageBusInterface::class);
// Dispatch the message
$bus->dispatch(new SendWelcomeEmail(userId: 123));
// Retrieve the in-memory transport configured in config/packages/test/messenger.yaml
/** @var InMemoryTransport $transport */
$transport = $container->get('messenger.transport.async');
// Assert message was sent
$this->assertCount(1, $transport->getSent());
$envelope = $transport->getSent()[0];
$message = $envelope->getMessage();
$this->assertInstanceOf(SendWelcomeEmail::class, $message);
$this->assertSame(123, $message->getUserId());
}
}Konfigurasi in-memory transport dalam config/packages/test/messenger.yaml untuk menangkap message yang di-dispatch tanpa pemrosesan queue yang sebenarnya.
Konfigurasi Code Coverage
Laporan code coverage PHPUnit mengidentifikasi path kode yang belum diuji. Konfigurasi coverage dalam phpunit.xml.dist.
<!-- phpunit.xml.dist -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true"
executionOrder="depends,defects"
cacheResult="true">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Integration">
<directory>tests/Integration</directory>
</testsuite>
<testsuite name="Functional">
<directory>tests/Functional</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
<exclude>
<directory>src/DataFixtures</directory>
<directory>src/Migrations</directory>
</exclude>
</source>
<coverage>
<report>
<html outputDirectory="var/coverage"/>
<clover outputFile="var/coverage/clover.xml"/>
</report>
</coverage>
</phpunit>Jalankan test dengan coverage menggunakan php bin/phpunit --coverage-html var/coverage. Laporan HTML menunjukkan status coverage baris per baris.
Kesimpulan
- Gunakan
TestCaseuntuk unit test murni tanpa dependensi container - Gunakan
KernelTestCaseketika test memerlukan service dari container - Gunakan
WebTestCaseuntuk simulasi HTTP request dan assertion response - Konfigurasi DAMADoctrineTestBundle untuk rollback transaksi otomatis antar test
- Uji autentikasi dengan
loginUser()daripada mensimulasikan form login - Gunakan
CommandTesteruntuk verifikasi console command dengan capture input/output - Konfigurasi in-memory transport di environment test untuk pengujian Messenger
- Organisasikan test ke dalam direktori Unit, Integration, dan Functional untuk kejelasan
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Tag
Bagikan
Artikel terkait

Symfony Live Components dan UX 3.0: Aplikasi Reaktif Tanpa JavaScript di 2026
Symfony Live Components memungkinkan pengembangan antarmuka reaktif dengan PHP dan Twig tanpa JavaScript. Tutorial lengkap LiveProp, LiveAction, form, dan deferred loading.

Doctrine ORM: Menguasai Relasi di Symfony
Panduan lengkap relasi Doctrine ORM di Symfony. OneToMany, ManyToMany, strategi pemuatan, dan optimasi performa dengan contoh praktis.

Pertanyaan Wawancara Symfony: Top 25 di 2026
25 pertanyaan wawancara Symfony paling sering ditanyakan. Arsitektur, Doctrine ORM, service, keamanan, form, dan testing dengan jawaban detail dan contoh kode.