Testes no Symfony em 2026: PHPUnit, KernelTestCase e Testes Funcionais

Guia completo para implementar testes em aplicações Symfony em 2026 com PHPUnit, KernelTestCase e testes funcionais. Melhores práticas e exemplos de código.

Testes no Symfony em 2026: PHPUnit, KernelTestCase e Testes Funcionais

Os testes automatizados representam o pilar fundamental de qualquer aplicação Symfony robusta e sustentável. Em 2026, o ecossistema de testes do Symfony alcançou uma maturidade excepcional, oferecendo aos desenvolvedores ferramentas poderosas para garantir a qualidade do código. Este guia explora as melhores práticas para implementar testes unitários, de integração e funcionais em projetos Symfony modernos.

Projetos Symfony com cobertura de testes superior a 80% apresentam em média 60% menos bugs em produção. O investimento em testes se paga desde as primeiras iterações do projeto.

Configuração do ambiente de testes

Antes de explorar os diferentes tipos de testes, é necessário configurar corretamente o ambiente. O Symfony 7 simplifica consideravelmente essa etapa graças ao Symfony Flex e às receitas automatizadas.

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

O arquivo phpunit.xml.dist na raiz do projeto define a configuração 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>

Testes unitários com PHPUnit

Os testes unitários verificam o comportamento isolado de classes e métodos. Eles executam rapidamente e não requerem dependências externas como banco de dados ou container de serviços.

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],
        ];
    }
}

O uso de data providers permite testar múltiplos cenários sem duplicar código de teste. O PHPUnit 11 melhora a sintaxe com atributos nativos do PHP 8.

Testes de integração com KernelTestCase

Os testes de integração verificam a interação entre múltiplos componentes. O KernelTestCase inicializa o kernel do Symfony e fornece acesso ao container de serviços.

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: 'João',
            lastName: 'Silva'
        );

        $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: 'João',
            lastName: 'Silva'
        );

        $this->expectException(\App\Exception\DuplicateEmailException::class);

        $this->registrationService->register(
            email: 'duplicate@example.com',
            password: 'AnotherPassword456!',
            firstName: 'Maria',
            lastName: 'Santos'
        );
    }

    protected function tearDown(): void
    {
        parent::tearDown();

        $this->entityManager->close();
    }
}

Isolamento do banco de dados

Para garantir o isolamento dos testes, existem várias estratégias. A mais eficiente utiliza transações com rollback automático:

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

Testes funcionais com WebTestCase

Os testes funcionais simulam requisições HTTP e verificam as respostas completas da aplicação. Eles testam toda a stack, do controller até a view.

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('Criar', [
            'product[name]' => 'Novo Produto',
            'product[price]' => '49.99',
            'product[description]' => 'Descrição do produto',
        ]);

        $this->assertResponseRedirects('/admin/products');
    }
}

Testes de APIs JSON

As aplicações modernas frequentemente expõem APIs REST ou GraphQL. O Symfony facilita os testes com asserções dedicadas:

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 e dublês de teste

Para isolar o código de dependências externas, os mocks são indispensáveis:

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);
    }
}

Pronto para mandar bem nas entrevistas de Symfony?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Organização dos testes e melhores práticas

Uma estrutura de testes clara facilita a manutenção a longo prazo:

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

Os testes devem seguir o padrão AAA (Arrange, Act, Assert) e cada teste deve verificar apenas um comportamento. Nomes de métodos explícitos documentam automaticamente o comportamento esperado.

Execução e integração contínua

O PHPUnit oferece diversas opções para executar testes de forma específica:

bash
# Executar todos os testes
./vendor/bin/phpunit

# Executar um arquivo específico
./vendor/bin/phpunit tests/Unit/Service/PriceCalculatorTest.php

# Executar testes com cobertura de código
./vendor/bin/phpunit --coverage-html coverage/

# Executar apenas testes rápidos
./vendor/bin/phpunit --testsuite=Unit

A integração com GitHub Actions ou GitLab CI automatiza a execução dos testes a cada 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

Conclusão

Os testes constituem um investimento essencial para qualquer projeto Symfony profissional. A combinação de testes unitários, de integração e funcionais oferece uma cobertura completa que assegura as evoluções do código. O PHPUnit 11, junto com as ferramentas do Symfony como KernelTestCase e WebTestCase, fornece um ambiente de testes maduro e eficiente.

A adoção progressiva de testes, começando pelas partes críticas da aplicação, permite construir uma base sólida. As equipes que investem em testes automatizados observam uma redução significativa de regressões e maior confiança durante os deploys.

Compartilhar

Artigos relacionados