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.
Tu saurais repérer le bug en Symfony ?
Un vrai bout de code, un bug caché, une tentative par jour. Sans compte pour essayer.

Écrit par
Anthony Fillion-MailletFondateur de SharpSkill
Développeur fullstack depuis plus de 10 ans. Il dirige SharpSkill et répond de tout ce qui y est publié.
Mis à jour le 15 juillet 2026
Partager
Articles similaires

Sécurité API REST Symfony en 2026 : OAuth2, Rate Limiting et Questions d'Entretien
Guide complet sur la securisation des API REST Symfony avec OAuth2, rate limiting, JWT et Voters. Inclut des questions d'entretien technique.

API Platform avec Symfony en 2026 : Architecture, State Providers et Questions d'Entretien
Maîtriser API Platform 4.2 avec Symfony : State Providers, Processors, Object Mapper, JSON Streamer pour des performances optimales, et questions d'entretien technique pour développeurs confirmés.

API Platform GraphQL avec Symfony : Schémas, Mutations et Questions d'Entretien 2026
Maîtrisez l'intégration de GraphQL dans Symfony avec API Platform. Ce guide couvre les schémas, mutations, résolveurs personnalisés et les questions d'entretien technique les plus posées en 2026.