Tests Symfony en 2026 : PHPUnit, KernelTestCase et Tests Fonctionnels
Guide complet pour tester les applications Symfony en 2026 avec PHPUnit, KernelTestCase et les tests fonctionnels. Bonnes pratiques et exemples de code.

Les tests automatisés constituent le pilier fondamental de toute application Symfony robuste et maintenable. En 2026, l'écosystème de test de Symfony a atteint une maturité exceptionnelle, offrant aux développeurs des outils puissants pour garantir la qualité du code. Ce guide explore les meilleures pratiques pour implémenter des tests unitaires, d'intégration et fonctionnels dans les projets Symfony modernes.
Les projets Symfony avec une couverture de tests supérieure à 80% présentent en moyenne 60% moins de bugs en production. L'investissement dans les tests se rentabilise dès les premières itérations du projet.
Configuration de l'environnement de test
Avant de plonger dans les différents types de tests, il convient de configurer correctement l'environnement. Symfony 7 simplifie considérablement cette étape grâce à Symfony Flex et aux recettes automatisées.
composer require --dev phpunit/phpunit symfony/test-packLe fichier phpunit.xml.dist à la racine du projet définit la configuration de base :
<?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"
cacheDirectory=".phpunit.cache">
<php>
<ini name="display_errors" value="1"/>
<ini name="error_reporting" value="-1"/>
<server name="APP_ENV" value="test" force="true"/>
<server name="SHELL_VERBOSITY" value="-1"/>
<server name="KERNEL_CLASS" value="App\Kernel"/>
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
</source>
</phpunit>Tests unitaires avec PHPUnit
Les tests unitaires vérifient le comportement isolé des classes et méthodes. Ils s'exécutent rapidement et ne nécessitent aucune dépendance externe comme la base de données ou le conteneur de services.
<?php
namespace App\Tests\Unit\Service;
use App\Service\PriceCalculator;
use App\Entity\Product;
use PHPUnit\Framework\TestCase;
class PriceCalculatorTest extends TestCase
{
private PriceCalculator $calculator;
protected function setUp(): void
{
$this->calculator = new PriceCalculator();
}
public function testCalculatePriceWithoutDiscount(): void
{
$product = new Product();
$product->setPrice(100.00);
$result = $this->calculator->calculate($product, quantity: 2);
$this->assertSame(200.00, $result);
}
public function testCalculatePriceWithPercentageDiscount(): void
{
$product = new Product();
$product->setPrice(100.00);
$result = $this->calculator->calculate(
$product,
quantity: 2,
discountPercent: 10
);
$this->assertSame(180.00, $result);
}
/**
* @dataProvider invalidQuantityProvider
*/
public function testCalculateThrowsExceptionForInvalidQuantity(int $quantity): void
{
$this->expectException(\InvalidArgumentException::class);
$product = new Product();
$product->setPrice(100.00);
$this->calculator->calculate($product, quantity: $quantity);
}
public static function invalidQuantityProvider(): array
{
return [
'zero quantity' => [0],
'negative quantity' => [-1],
];
}
}L'utilisation des data providers permet de tester plusieurs scénarios sans dupliquer le code de test. PHPUnit 11 améliore la syntaxe avec les attributs PHP 8 natifs.
Tests d'intégration avec KernelTestCase
Les tests d'intégration vérifient l'interaction entre plusieurs composants. KernelTestCase démarre le kernel Symfony et donne accès au conteneur de services.
<?php
namespace App\Tests\Integration\Service;
use App\Entity\User;
use App\Service\UserRegistrationService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class UserRegistrationServiceTest extends KernelTestCase
{
private UserRegistrationService $registrationService;
private EntityManagerInterface $entityManager;
protected function setUp(): void
{
self::bootKernel();
$container = static::getContainer();
$this->registrationService = $container->get(UserRegistrationService::class);
$this->entityManager = $container->get(EntityManagerInterface::class);
}
public function testRegisterNewUser(): void
{
$user = $this->registrationService->register(
email: 'test@example.com',
password: 'SecurePassword123!',
firstName: 'Jean',
lastName: 'Dupont'
);
$this->assertInstanceOf(User::class, $user);
$this->assertNotNull($user->getId());
$this->assertSame('test@example.com', $user->getEmail());
$this->assertTrue($user->isActive());
}
public function testRegisterDuplicateEmailThrowsException(): void
{
$this->registrationService->register(
email: 'duplicate@example.com',
password: 'SecurePassword123!',
firstName: 'Jean',
lastName: 'Dupont'
);
$this->expectException(\App\Exception\DuplicateEmailException::class);
$this->registrationService->register(
email: 'duplicate@example.com',
password: 'AnotherPassword456!',
firstName: 'Marie',
lastName: 'Martin'
);
}
protected function tearDown(): void
{
parent::tearDown();
$this->entityManager->close();
}
}Isolation de la base de données
Pour garantir l'isolation des tests, plusieurs stratégies existent. La plus efficace utilise les transactions avec rollback automatique :
<?php
namespace App\Tests;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
abstract class DatabaseTestCase extends KernelTestCase
{
protected EntityManagerInterface $entityManager;
protected function setUp(): void
{
self::bootKernel();
$this->entityManager = static::getContainer()->get(EntityManagerInterface::class);
$this->entityManager->beginTransaction();
}
protected function tearDown(): void
{
$this->entityManager->rollback();
$this->entityManager->close();
parent::tearDown();
}
}Tests fonctionnels avec WebTestCase
Les tests fonctionnels simulent les requêtes HTTP et vérifient les réponses complètes de l'application. Ils testent l'ensemble de la stack, du contrôleur jusqu'à la vue.
<?php
namespace App\Tests\Functional\Controller;
use App\Factory\UserFactory;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;
class ProductControllerTest extends WebTestCase
{
use Factories;
use ResetDatabase;
public function testListProductsReturnsSuccessResponse(): void
{
$client = static::createClient();
$client->request('GET', '/products');
$this->assertResponseIsSuccessful();
$this->assertSelectorExists('h1');
}
public function testShowProductDisplaysCorrectInformation(): void
{
$client = static::createClient();
$client->request('GET', '/products/symfony-book');
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('h1', 'Symfony Book');
$this->assertSelectorExists('.product-price');
}
public function testCreateProductRequiresAuthentication(): void
{
$client = static::createClient();
$client->request('GET', '/admin/products/new');
$this->assertResponseRedirects('/login');
}
public function testAuthenticatedUserCanCreateProduct(): void
{
$client = static::createClient();
$user = UserFactory::createOne(['roles' => ['ROLE_ADMIN']]);
$client->loginUser($user->_real());
$client->request('GET', '/admin/products/new');
$this->assertResponseIsSuccessful();
$client->submitForm('Créer', [
'product[name]' => 'Nouveau Produit',
'product[price]' => '49.99',
'product[description]' => 'Description du produit',
]);
$this->assertResponseRedirects('/admin/products');
}
}Tests des API JSON
Les applications modernes exposent souvent des API REST ou GraphQL. Symfony facilite leur test avec des assertions dédiées :
<?php
namespace App\Tests\Functional\Api;
use App\Factory\ProductFactory;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;
class ProductApiTest extends WebTestCase
{
use Factories;
use ResetDatabase;
public function testGetProductsReturnsJsonResponse(): void
{
ProductFactory::createMany(3);
$client = static::createClient();
$client->request('GET', '/api/products', [], [], [
'HTTP_ACCEPT' => 'application/json',
]);
$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('Content-Type', 'application/json');
$data = json_decode($client->getResponse()->getContent(), true);
$this->assertCount(3, $data['products']);
}
public function testCreateProductWithValidData(): void
{
$client = static::createClient();
$client->request('POST', '/api/products', [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode([
'name' => 'API Product',
'price' => 29.99,
]));
$this->assertResponseStatusCodeSame(201);
$data = json_decode($client->getResponse()->getContent(), true);
$this->assertArrayHasKey('id', $data);
$this->assertSame('API Product', $data['name']);
}
public function testCreateProductWithInvalidDataReturnsBadRequest(): void
{
$client = static::createClient();
$client->request('POST', '/api/products', [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode([
'name' => '',
'price' => -10,
]));
$this->assertResponseStatusCodeSame(400);
$data = json_decode($client->getResponse()->getContent(), true);
$this->assertArrayHasKey('errors', $data);
}
}Mocking et doubles de test
Pour isoler le code des dépendances externes, les mocks s'avèrent indispensables :
<?php
namespace App\Tests\Unit\Service;
use App\Service\NotificationService;
use App\Service\PaymentGateway;
use App\Service\OrderProcessor;
use App\Entity\Order;
use PHPUnit\Framework\TestCase;
class OrderProcessorTest extends TestCase
{
public function testProcessOrderCallsPaymentGateway(): void
{
$paymentGateway = $this->createMock(PaymentGateway::class);
$notificationService = $this->createMock(NotificationService::class);
$paymentGateway
->expects($this->once())
->method('charge')
->with(
$this->equalTo(99.99),
$this->isType('string')
)
->willReturn(true);
$notificationService
->expects($this->once())
->method('sendOrderConfirmation');
$processor = new OrderProcessor($paymentGateway, $notificationService);
$order = new Order();
$order->setTotal(99.99);
$result = $processor->process($order);
$this->assertTrue($result);
}
}Prêt à réussir tes entretiens Symfony ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Organisation des tests et bonnes pratiques
Une structure de tests claire facilite la maintenance à long terme :
tests/
├── Unit/
│ ├── Entity/
│ ├── Service/
│ └── Util/
├── Integration/
│ ├── Repository/
│ └── Service/
├── Functional/
│ ├── Controller/
│ └── Api/
└── bootstrap.phpLes tests doivent respecter le pattern AAA (Arrange, Act, Assert) et chaque test ne doit vérifier qu'un seul comportement. Les noms de méthodes explicites documentent automatiquement le comportement attendu.
Exécution et intégration continue
PHPUnit offre de nombreuses options pour exécuter les tests de manière ciblée :
# Exécuter tous les tests
./vendor/bin/phpunit
# Exécuter un fichier spécifique
./vendor/bin/phpunit tests/Unit/Service/PriceCalculatorTest.php
# Exécuter les tests avec couverture de code
./vendor/bin/phpunit --coverage-html coverage/
# Exécuter uniquement les tests rapides
./vendor/bin/phpunit --testsuite=UnitL'intégration avec GitHub Actions ou GitLab CI automatise l'exécution des tests à chaque push :
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: password
POSTGRES_DB: test_db
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: xdebug
- run: composer install
- run: ./vendor/bin/phpunit --coverage-clover coverage.xmlConclusion
Les tests constituent un investissement essentiel pour tout projet Symfony professionnel. La combinaison des tests unitaires, d'intégration et fonctionnels offre une couverture complète qui sécurise les évolutions du code. PHPUnit 11, couplé aux outils Symfony comme KernelTestCase et WebTestCase, fournit un environnement de test mature et performant.
L'adoption progressive des tests, en commençant par les parties critiques de l'application, permet de construire une base solide. Les équipes qui investissent dans les tests automatisés constatent une réduction significative des régressions et une confiance accrue lors des déploiements.
Partager
Articles similaires

Injection de dépendances Symfony : conteneur de services, autowiring et tags en 2026
Plongée dans l'injection de dépendances Symfony : conteneur de services, autowiring, attribut #[Autowire], liaison d'interfaces, services taggés avec #[AutowireIterator], compiler passes et services paresseux dans Symfony 7.4 et 8.0.

Symfony et Docker en 2026 : environnement de développement et déploiement en production
Un workflow Symfony Docker complet pour 2026 : environnement de développement FrankenPHP, image de production multi-étapes, mode worker, secrets chiffrés, optimisation de l'image et déploiements sans interruption avec migrations.

Sécurité Symfony en 2026 : Voters, Firewalls et Questions d'Entretien Technique
Plongée technique dans la sécurité Symfony couvrant les voters, firewalls, access tokens et authentification, avec les questions d'entretien les plus fréquentes pour les développeurs Symfony.