Testing in Symfony 2026: PHPUnit, KernelTestCase e Test Funzionali

Guida completa alle strategie di testing moderne in Symfony con PHPUnit 12, KernelTestCase e test funzionali per applicazioni robuste e affidabili.

Testing in Symfony con PHPUnit e KernelTestCase

Il testing del software moderno non rappresenta più una funzionalità opzionale, ma un requisito fondamentale per le applicazioni Symfony professionali. Nel 2026, l'ecosistema di testing di Symfony si è evoluto ulteriormente, offrendo agli sviluppatori strumenti potenti come PHPUnit 12, KernelTestCase e capacità avanzate per i test funzionali.

Symfony 7.2+ introduce strumenti di testing migliorati, tra cui performance ottimizzate per KernelTestCase e integrazione nativa con PHPUnit 12. Queste funzionalità consentono un'esecuzione dei test più rapida e un migliore isolamento.

Perché il Testing in Symfony è Indispensabile

La complessità delle moderne applicazioni web richiede una rete di sicurezza robusta. Le applicazioni Symfony interagiscono con database, API esterne, code di messaggi e numerosi altri componenti. Senza test automatizzati, ogni modifica al codice diventa un rischio.

I test svolgono diverse funzioni critiche:

  • Prevenzione delle regressioni: Le modifiche non compromettono le funzionalità esistenti
  • Documentazione: I test descrivono il comportamento atteso del codice
  • Sicurezza nel refactoring: Il codice può essere migliorato senza timore di errori non rilevati
  • Fiducia nel deployment: Le pipeline automatizzate possono effettuare deploy sicuri

Configurazione di PHPUnit 12 in Symfony

PHPUnit 12 porta miglioramenti significativi per i progetti Symfony. L'installazione avviene tramite Composer:

bash
composer require --dev phpunit/phpunit:^12.0
composer require --dev symfony/test-pack

La configurazione viene definita nel file phpunit.xml.dist:

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"
         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 Test: La Base della Piramide dei Test

Gli unit test costituiscono le fondamenta di ogni strategia di testing. Testano unità isolate senza dipendenze esterne:

php
<?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: Integrazione con il Container di Symfony

KernelTestCase consente l'accesso al service container di Symfony durante i test. Questo è essenziale per i test di integrazione che richiedono servizi reali:

php
<?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();
    }
}

Test del Database con Transazioni

Per i test che interagiscono con il database, si raccomanda l'uso delle transazioni per l'isolamento:

php
<?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: Test Funzionali per i Controller

I test funzionali simulano richieste HTTP e verificano il comportamento dell'intera applicazione:

php
<?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']);
    }
}

Test delle API con Assertion JSON

Le moderne applicazioni Symfony sono spesso API-first. Assertion specifiche facilitano il testing delle response JSON:

php
<?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 dei Servizi Esterni

Le dipendenze esterne devono essere sostituite da mock nei test:

php
<?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 Fixture e Factory

Per dati di test consistenti, si raccomandano le classi factory:

php
<?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 e Metriche di Qualità

La code coverage dovrebbe essere una guida, non un obiettivo assoluto:

bash
# Generare report di coverage
php bin/phpunit --coverage-html coverage/

# Coverage con requisito minimo
php bin/phpunit --coverage-text --coverage-filter src/ --min-coverage=80

Pronto a superare i tuoi colloqui su Symfony?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Continuous Integration

I test devono essere eseguiti automaticamente nella pipeline CI/CD:

yaml
# .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_test

Conclusione

Il testing efficace in Symfony richiede una combinazione ponderata di diversi tipi di test. Gli unit test garantiscono la correttezza dei singoli componenti, mentre i test di integrazione basati su KernelTestCase validano l'interazione con il container di Symfony. I test funzionali con WebTestCase simulano interazioni reali degli utenti e assicurano che l'applicazione funzioni nel suo complesso.

L'investimento in una solida infrastruttura di testing ripaga attraverso una maggiore qualità del codice, cicli di sviluppo più rapidi e deployment sicuri. Con PHPUnit 12 e gli strumenti di testing avanzati di Symfony 7.2, gli sviluppatori dispongono di un ecosistema maturo che consente un testing professionale al massimo livello.

Sfida del giorno

Sapresti trovare il bug in Symfony?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore di SharpSkill

Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.

Aggiornato il 15 luglio 2026

Tag

#symfony
#phpunit
#testing
#kerneltestcase
#functional-tests

Condividi

Articoli correlati