Symfony Testen in 2026: PHPUnit, KernelTestCase en Functionele Tests
Een uitgebreide gids voor moderne teststrategieën in Symfony met PHPUnit 12, KernelTestCase en functionele tests voor robuuste applicaties.

Modern software testen is geen optionele functie meer, maar een fundamentele vereiste voor professionele Symfony-applicaties. In 2026 is het testing-ecosysteem van Symfony verder geëvolueerd en biedt ontwikkelaars krachtige tools zoals PHPUnit 12, KernelTestCase en geavanceerde functionele testmogelijkheden.
Symfony 7.2+ introduceert verbeterde testing-tools, waaronder geoptimaliseerde KernelTestCase-prestaties en naadloze PHPUnit 12-integratie. Deze functies maken snellere testuitvoering en betere isolatie mogelijk.
Waarom Testen in Symfony Onmisbaar Is
De complexiteit van moderne webapplicaties vereist een robuust vangnet. Symfony-applicaties communiceren met databases, externe API's, message queues en talloze andere componenten. Zonder geautomatiseerde tests wordt elke codewijziging een risico.
Tests vervullen verschillende kritieke functies:
- Regressiepreventie: Wijzigingen breken geen bestaande functionaliteit
- Documentatie: Tests beschrijven het verwachte gedrag van de code
- Refactoring-veiligheid: Code kan worden verbeterd zonder angst voor onontdekte fouten
- Deployment-vertrouwen: Geautomatiseerde pipelines kunnen veilig deployen
PHPUnit 12 Configureren in Symfony
PHPUnit 12 brengt significante verbeteringen voor Symfony-projecten. De installatie gebeurt via Composer:
composer require --dev phpunit/phpunit:^12.0
composer require --dev symfony/test-packDe configuratie wordt gedefinieerd in 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"
requireCoverageMetadata="true"
beStrictAboutOutputDuringTests="true"
displayDetailsOnTestsThatTriggerWarnings="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>
<coverage>
<report>
<html outputDirectory="coverage"/>
</report>
</coverage>
<source>
<include>
<directory>src</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="test"/>
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak"/>
</php>
</phpunit>Unit Tests: De Basis van de Testpiramide
Unit tests vormen het fundament van elke teststrategie. Ze testen geïsoleerde eenheden zonder externe afhankelijkheden:
<?php
namespace App\Tests\Unit\Service;
use App\Service\PriceCalculator;
use App\Entity\Product;
use App\Entity\Discount;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\DataProvider;
class PriceCalculatorTest extends TestCase
{
private PriceCalculator $calculator;
protected function setUp(): void
{
$this->calculator = new PriceCalculator();
}
#[Test]
public function calculatesPriceWithoutDiscount(): void
{
$product = new Product();
$product->setPrice(100.00);
$result = $this->calculator->calculate($product);
$this->assertSame(100.00, $result);
}
#[Test]
public function appliesPercentageDiscount(): void
{
$product = new Product();
$product->setPrice(100.00);
$discount = new Discount();
$discount->setType('percentage');
$discount->setValue(20);
$result = $this->calculator->calculate($product, $discount);
$this->assertSame(80.00, $result);
}
#[Test]
#[DataProvider('discountProvider')]
public function handlesVariousDiscountScenarios(
float $price,
string $type,
float $value,
float $expected
): void {
$product = new Product();
$product->setPrice($price);
$discount = new Discount();
$discount->setType($type);
$discount->setValue($value);
$result = $this->calculator->calculate($product, $discount);
$this->assertSame($expected, $result);
}
public static function discountProvider(): array
{
return [
'percentage_10' => [100.00, 'percentage', 10, 90.00],
'percentage_50' => [200.00, 'percentage', 50, 100.00],
'fixed_amount' => [100.00, 'fixed', 25, 75.00],
'zero_discount' => [100.00, 'percentage', 0, 100.00],
];
}
}KernelTestCase: Integratie met de Symfony Container
KernelTestCase biedt toegang tot de Symfony service container tijdens tests. Dit is essentieel voor integratietests die echte services nodig hebben:
<?php
namespace App\Tests\Integration\Service;
use App\Service\UserRegistrationService;
use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use PHPUnit\Framework\Attributes\Test;
class UserRegistrationServiceTest extends KernelTestCase
{
private UserRegistrationService $service;
private UserRepository $userRepository;
protected function setUp(): void
{
self::bootKernel();
$container = static::getContainer();
$this->service = $container->get(UserRegistrationService::class);
$this->userRepository = $container->get(UserRepository::class);
}
#[Test]
public function registersNewUser(): void
{
$userData = [
'email' => 'test@example.com',
'password' => 'SecurePassword123!',
'name' => 'Test User',
];
$user = $this->service->register($userData);
$this->assertNotNull($user->getId());
$this->assertSame('test@example.com', $user->getEmail());
$this->assertTrue($user->isActive());
}
#[Test]
public function throwsExceptionForDuplicateEmail(): void
{
$userData = [
'email' => 'duplicate@example.com',
'password' => 'SecurePassword123!',
'name' => 'First User',
];
$this->service->register($userData);
$this->expectException(\App\Exception\DuplicateEmailException::class);
$this->service->register($userData);
}
protected function tearDown(): void
{
parent::tearDown();
}
}Database Tests met Transacties
Voor tests die met de database communiceren, wordt het gebruik van transacties aanbevolen voor isolatie:
<?php
namespace App\Tests\Integration\Repository;
use App\Entity\Article;
use App\Repository\ArticleRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use PHPUnit\Framework\Attributes\Test;
class ArticleRepositoryTest extends KernelTestCase
{
private EntityManagerInterface $entityManager;
private ArticleRepository $repository;
protected function setUp(): void
{
self::bootKernel();
$container = static::getContainer();
$this->entityManager = $container->get(EntityManagerInterface::class);
$this->repository = $container->get(ArticleRepository::class);
$this->entityManager->beginTransaction();
}
#[Test]
public function findsPublishedArticles(): void
{
$article = new Article();
$article->setTitle('Test Article');
$article->setStatus('published');
$article->setPublishedAt(new \DateTimeImmutable());
$this->entityManager->persist($article);
$this->entityManager->flush();
$results = $this->repository->findPublished();
$this->assertCount(1, $results);
$this->assertSame('Test Article', $results[0]->getTitle());
}
#[Test]
public function excludesDraftArticles(): void
{
$draft = new Article();
$draft->setTitle('Draft Article');
$draft->setStatus('draft');
$published = new Article();
$published->setTitle('Published Article');
$published->setStatus('published');
$published->setPublishedAt(new \DateTimeImmutable());
$this->entityManager->persist($draft);
$this->entityManager->persist($published);
$this->entityManager->flush();
$results = $this->repository->findPublished();
$this->assertCount(1, $results);
}
protected function tearDown(): void
{
$this->entityManager->rollback();
parent::tearDown();
}
}WebTestCase: Functionele Tests voor Controllers
Functionele tests simuleren HTTP-verzoeken en verifiëren het gedrag van de gehele applicatie:
<?php
namespace App\Tests\Functional\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Response;
use PHPUnit\Framework\Attributes\Test;
class ArticleControllerTest extends WebTestCase
{
#[Test]
public function listArticlesReturnsOk(): void
{
$client = static::createClient();
$client->request('GET', '/api/articles');
$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('content-type', 'application/json');
}
#[Test]
public function createArticleRequiresAuthentication(): void
{
$client = static::createClient();
$client->request('POST', '/api/articles', [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode([
'title' => 'New Article',
'content' => 'Article content',
]));
$this->assertResponseStatusCodeSame(Response::HTTP_UNAUTHORIZED);
}
#[Test]
public function authenticatedUserCanCreateArticle(): void
{
$client = static::createClient();
$client->loginUser($this->getTestUser());
$client->request('POST', '/api/articles', [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode([
'title' => 'New Article',
'content' => 'Article content',
]));
$this->assertResponseStatusCodeSame(Response::HTTP_CREATED);
$responseData = json_decode($client->getResponse()->getContent(), true);
$this->assertArrayHasKey('id', $responseData);
$this->assertSame('New Article', $responseData['title']);
}
private function getTestUser(): object
{
$container = static::getContainer();
$userRepository = $container->get(\App\Repository\UserRepository::class);
return $userRepository->findOneBy(['email' => 'test@example.com']);
}
}API Tests met JSON Assertions
Moderne Symfony-applicaties zijn vaak API-first. Specifieke assertions vergemakkelijken het testen van JSON responses:
<?php
namespace App\Tests\Functional\Api;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use PHPUnit\Framework\Attributes\Test;
class ProductApiTest extends WebTestCase
{
#[Test]
public function getProductReturnsCorrectStructure(): void
{
$client = static::createClient();
$client->request('GET', '/api/products/1');
$this->assertResponseIsSuccessful();
$this->assertJsonContains([
'id' => 1,
'type' => 'product',
]);
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertArrayHasKey('name', $content);
$this->assertArrayHasKey('price', $content);
$this->assertArrayHasKey('currency', $content);
$this->assertIsFloat($content['price']);
}
#[Test]
public function searchProductsWithFilters(): void
{
$client = static::createClient();
$client->request('GET', '/api/products', [
'category' => 'electronics',
'min_price' => 100,
'max_price' => 500,
]);
$this->assertResponseIsSuccessful();
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertArrayHasKey('data', $content);
$this->assertArrayHasKey('meta', $content);
$this->assertIsArray($content['data']);
}
#[Test]
public function invalidProductIdReturns404(): void
{
$client = static::createClient();
$client->request('GET', '/api/products/99999');
$this->assertResponseStatusCodeSame(404);
}
}Mocking van Externe Services
Externe afhankelijkheden moeten worden vervangen door mocks in tests:
<?php
namespace App\Tests\Integration\Service;
use App\Service\PaymentService;
use App\Service\PaymentGatewayInterface;
use App\Entity\Order;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use PHPUnit\Framework\Attributes\Test;
class PaymentServiceTest extends KernelTestCase
{
#[Test]
public function processesPaymentSuccessfully(): void
{
self::bootKernel();
$container = static::getContainer();
$gatewayMock = $this->createMock(PaymentGatewayInterface::class);
$gatewayMock->expects($this->once())
->method('charge')
->with(
$this->equalTo(99.99),
$this->equalTo('EUR'),
$this->isType('string')
)
->willReturn(['status' => 'success', 'transaction_id' => 'txn_123']);
$container->set(PaymentGatewayInterface::class, $gatewayMock);
$paymentService = $container->get(PaymentService::class);
$order = new Order();
$order->setTotal(99.99);
$order->setCurrency('EUR');
$result = $paymentService->process($order);
$this->assertTrue($result->isSuccessful());
$this->assertSame('txn_123', $result->getTransactionId());
}
#[Test]
public function handlesPaymentFailure(): void
{
self::bootKernel();
$container = static::getContainer();
$gatewayMock = $this->createMock(PaymentGatewayInterface::class);
$gatewayMock->method('charge')
->willThrowException(new \App\Exception\PaymentDeclinedException('Card declined'));
$container->set(PaymentGatewayInterface::class, $gatewayMock);
$paymentService = $container->get(PaymentService::class);
$order = new Order();
$order->setTotal(99.99);
$order->setCurrency('EUR');
$this->expectException(\App\Exception\PaymentDeclinedException::class);
$paymentService->process($order);
}
}Test Fixtures en Factories
Voor consistente testdata worden factory-klassen aanbevolen:
<?php
namespace App\Tests\Factory;
use App\Entity\User;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class UserFactory
{
private static int $counter = 0;
public function __construct(
private readonly UserPasswordHasherInterface $passwordHasher
) {}
public function create(array $attributes = []): User
{
self::$counter++;
$user = new User();
$user->setEmail($attributes['email'] ?? "user" . self::$counter . "@test.com");
$user->setName($attributes['name'] ?? 'Test User ' . self::$counter);
$user->setRoles($attributes['roles'] ?? ['ROLE_USER']);
$password = $attributes['password'] ?? 'password123';
$hashedPassword = $this->passwordHasher->hashPassword($user, $password);
$user->setPassword($hashedPassword);
return $user;
}
public function createAdmin(array $attributes = []): User
{
return $this->create(array_merge($attributes, [
'roles' => ['ROLE_ADMIN'],
]));
}
}Code Coverage en Kwaliteitsmetrieken
Code coverage moet dienen als richtlijn, niet als absoluut doel:
# Coverage rapport genereren
php bin/phpunit --coverage-html coverage/
# Coverage met minimale vereiste
php bin/phpunit --coverage-text --coverage-filter src/ --min-coverage=80Klaar om je Symfony gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Continuous Integration
Tests moeten automatisch worden uitgevoerd in de CI/CD-pipeline:
# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app_test
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: xdebug
- name: Install dependencies
run: composer install --prefer-dist
- name: Run tests
run: php bin/phpunit --coverage-text
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/app_testConclusie
Effectief testen in Symfony vereist een doordachte combinatie van verschillende testsoorten. Unit tests garanderen de correctheid van individuele componenten, terwijl KernelTestCase-gebaseerde integratietests de samenwerking met de Symfony container valideren. Functionele tests met WebTestCase simuleren echte gebruikersinteracties en zorgen ervoor dat de applicatie als geheel functioneert.
De investering in een solide testinfrastructuur betaalt zich terug door verhoogde codekwaliteit, snellere ontwikkelcycli en veilige deployments. Met PHPUnit 12 en de uitgebreide testing-tools van Symfony 7.2 beschikken ontwikkelaars over een volwassen ecosysteem dat professioneel testen op het hoogste niveau mogelijk maakt.
Tags
Delen
Gerelateerde artikelen

Doctrine ORM: Relaties beheersen in Symfony
Volledige gids voor Doctrine ORM-relaties in Symfony. OneToMany, ManyToMany, laadstrategieën en performance-optimalisatie met praktische voorbeelden.

Symfony-sollicitatievragen: Top 25 in 2026
De 25 meest gestelde Symfony-sollicitatievragen. Architectuur, Doctrine ORM, services, beveiliging, formulieren en tests met gedetailleerde antwoorden en codevoorbeelden.

Symfony 7: API Platform en Best Practices
Volledige gids voor het bouwen van professionele REST API's met Symfony 7 en API Platform 4. State Providers, Processors, validatie en serialisatie uitgelegd met praktische voorbeelden.