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.

Tests Symfony en 2026 : PHPUnit, KernelTestCase et Tests Fonctionnels

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.

bash
composer require --dev phpunit/phpunit symfony/test-pack

Le fichier phpunit.xml.dist à la racine du projet définit la configuration de base :

xml
<?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
<?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
<?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
<?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
<?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
<?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
<?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 :

text
tests/
├── Unit/
│   ├── Entity/
│   ├── Service/
│   └── Util/
├── Integration/
│   ├── Repository/
│   └── Service/
├── Functional/
│   ├── Controller/
│   └── Api/
└── bootstrap.php

Les 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 :

bash
# 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=Unit

L'intégration avec GitHub Actions ou GitLab CI automatise l'exécution des tests à chaque push :

yaml
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.xml

Conclusion

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