# Testes de Integração no Symfony com Testcontainers

*Disclaimer: Eu não sou uma entidade divina. O que eu falo não é uma verdade absoluta. Não tenha medo de questionar até o mundo, pois ele pode estar errado, não você.*

Hoje não é segredo para ninguém a importância dos testes automatizados para manter a qualidade e integridade do seu software e normalmente falamos muito de testes unitários, porém, hoje, iremos focar mais em testes de integração no [Symfony](https://symfony.com/) Framework.

### Estou sem paciência, mostre me o código!

Okay okay! Caso você não esteja com paciência para ler este artigo, eu tenho um projeto de teste com a implementação deste artigo no link abaixo.

[https://github.com/joubertredrat/symfony-testcontainers](https://github.com/joubertredrat/symfony-testcontainers)

### Symfony Framework e testes de Integração

Hoje o [Symfony](https://symfony.com/) Framework é um dos frameworks mais maduros do universo do [PHP](https://www.php.net) e ele tem várias soluções bem implementadas, como para testes de integração por exemplo. Porém, pessoalmente eu sempre achei que embora existe uma facilidade de fazer os testes de integração em si, fornecer as dependências externas para os testes nem sempre foi tão fácil, como banco de dados por exemplo.

Mesmo com o surgimento do [Docker](https://www.docker.com/), eu ainda percebia a necessidade de provisionar as dependências externas de alguma forma para os testes, porém, existe uma solução muito interessante que pode facilitar muito esta etapa, o [Testcontainers](https://testcontainers.com/).

### Testcontainers

[Testcontainers](https://testcontainers.com/) é um framework open source que permite você provisionar de forma mais facilitada qualquer dependência externa que você precise usando [Docker](https://www.docker.com/), como banco de dados, broker de mensagens, sistemas de cache, ou praticamente qualquer dependência em container.

O grande diferencial do [Testcontainers](https://testcontainers.com/) em relação a [Docker](https://www.docker.com/) compose ou qualquer outra forma de provisionamento de containers é que você consegue programar o provisionamento do container, sendo que hoje já tem suporte para [Golang](https://go.dev/), [Java](https://dev.java/), [.NET](https://dotnet.microsoft.com/), [Node.js](https://nodejs.org), [Python](https://www.python.org/), [Rust](https://www.rust-lang.org), várias outras linguagens, e é claro, não poderia faltar, o [PHP](https://www.php.net)!

O meu primeiro contato com [Testcontainers](https://testcontainers.com/) foi com um projeto em [Golang](https://go.dev/) e eu gostei muito da facilidade do provisionamento de um container [MongoDB](https://www.mongodb.com/) para fazer meus testes nos repositórios e após isto, decidi fazer o mesmo em um projeto pessoal que eu tenho em [PHP](https://www.php.net) usando o [Symfony](https://symfony.com/) Framework.

### Symfony + Testcontainers = ❤️

Uma das grandes vantagens do [Symfony](https://symfony.com/) é justamente o suporte para os testes no [PHPUnit](https://phpunit.de) por fornecer um Kernel totalmente funcional para fazer o bootstrap necessário para os testes.

Embora [Testcontainers](https://testcontainers.com/) tenha suporte para [PHP](https://www.php.net), a implementação é mais recente e você pode conferir ele em [https://github.com/testcontainers/testcontainers-php](https://github.com/testcontainers/testcontainers-php).

Abaixo temos uma implementação de um container do [MySQL](https://www.mysql.com/) 8.0, que é a dependência externa deste projeto, além do boot do Kernel do [Symfony](https://symfony.com/), criação do banco de dados e do schema.

```php
class IntegrationTestCase extends KernelTestCase
{
    protected static ?MySQLContainer $container = null;

    public static function setUpBeforeClass(): void
    {
        parent::setUpBeforeClass();

        if (!static::$container) {
            static::$container = MySQLContainer::make('8.0', 'password');
            static::$container->withPort('19306', '3306');
            static::$container->run();

            $kernel = self::bootKernel();
            $container = $kernel->getContainer();

            $application = new Application($kernel);
            $application->setAutoExit(false);

            $application->run(
                new ArrayInput(['command' => 'doctrine:database:create', '--if-not-exists' => true])
            );

            $entityManager = $container->get('doctrine')->getManager();
            $metadata = $entityManager->getMetadataFactory()->getAllMetadata();
            $schemaTool = new SchemaTool($entityManager);
            $schemaTool->dropSchema($metadata);
            $schemaTool->createSchema($metadata);
        }
    }

    public static function tearDownAfterClass(): void
    {
        parent::tearDownAfterClass();

        if (static::$container instanceof MySQLContainer) {
            static::$container->remove();
        }
    }
```

Com isto, temos a classe base para as classes que irão de fato executar os testes, como no exemplo abaixo.

```php
class UserRepositoryTest extends IntegrationTestCase
{
    public function testSave(): void
    {
        $user = new User();
        $user->setName('John Doe');
        $user->setEmail('john@doe.local');

        $repo = $this->getRepository();
        $repo->save($user, true);

        self::assertNotNull($user->getId());
        self::assertIsInt($user->getId());
        self::assertTrue($user->getId() > 0);
    }

    public function testGetByEmail(): void
    {
        $user = new User();
        $user->setName('John Doe');
        $user->setEmail('john2@doe.local');

        $repo = $this->getRepository();
        $userNotFound = $repo->getByEmail($user->getEmail());
        self::assertNull($userNotFound);

        $repo->save($user, true);
        $userFound = $repo->getByEmail($user->getEmail());

        self::assertEquals($user->getEmail(), $userFound->getEmail());
    }

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

        $connection = $this
            ->getContainer()
            ->get('doctrine')
            ->getManager()
            ->getConnection()
        ;
        $connection->executeStatement('TRUNCATE TABLE users');
    }

    protected function getRepository(): UserRepository
    {
        return $this->getContainer()->get(UserRepository::class);
    }
}
```

Ao executar a suite de testes, você vai notar uma demora para finalizar os testes, porém, isto é normal, porque durante este processo, o [Testcontainers](https://testcontainers.com/) está provisionando o container que você definiu para usar nos testes.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1724941514944/5e2096d2-e937-4ecc-8964-83c26ee5dd2a.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1724941522389/c9eca5f9-7938-44a0-a7cb-532299aff6a3.png align="center")

Por fim, com essa facilidade, dá para até tentar fazer coisas loucas, como 100% de coverage. Não acredita? Você mesmo pode ver em [https://joubertredrat.github.io/symfony-testcontainers](https://joubertredrat.github.io/symfony-testcontainers).

**UPDATE 12/11:** A princípio, nos exemplos acima eu usei o `setUpBeforeClass()` e `tearDownAfterClass()` para controlar o estado de funcionamento do container para os testes, porém, depois eu descobri que esses métodos executam a cada nova classe de testes, o que não é o que desejamos.

Por isso corrigi o projeto para passar a usar Extensions e Subscribers do [PHPUnit](https://phpunit.de/) na versão 11, e isto resolveu o problema, fazendo com que o container inicie antes e pare depois de toda a suite de testes.

Dependendo da versão do [PHPUnit](https://phpunit.de/) você usar, pode ser necessário uma abordagem diferente.

Então é isto, até a próxima!
