Testes de Integração no Symfony com Testcontainers

Joubert RedRatJoubert RedRat
4 min read

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

Symfony Framework e testes de Integração

Hoje o Symfony Framework é um dos frameworks mais maduros do universo do PHP 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, 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.

Testcontainers

Testcontainers é um framework open source que permite você provisionar de forma mais facilitada qualquer dependência externa que você precise usando Docker, como banco de dados, broker de mensagens, sistemas de cache, ou praticamente qualquer dependência em container.

O grande diferencial do Testcontainers em relação a Docker 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, Java, .NET, Node.js, Python, Rust, várias outras linguagens, e é claro, não poderia faltar, o PHP!

O meu primeiro contato com Testcontainers foi com um projeto em Golang e eu gostei muito da facilidade do provisionamento de um container MongoDB para fazer meus testes nos repositórios e após isto, decidi fazer o mesmo em um projeto pessoal que eu tenho em PHP usando o Symfony Framework.

Symfony + Testcontainers = ❤️

Uma das grandes vantagens do Symfony é justamente o suporte para os testes no PHPUnit por fornecer um Kernel totalmente funcional para fazer o bootstrap necessário para os testes.

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

Abaixo temos uma implementação de um container do MySQL 8.0, que é a dependência externa deste projeto, além do boot do Kernel do Symfony, criação do banco de dados e do schema.

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.

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 está provisionando o container que você definiu para usar nos testes.

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.

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 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 você usar, pode ser necessário uma abordagem diferente.

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

0
Subscribe to my newsletter

Read articles from Joubert RedRat directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Joubert RedRat
Joubert RedRat

I am eternal trainee of life that works over 10 years ago in technology area, with focus in engineering and architecture of software and with specialization in financial systems and DevOps, moreover speaker around the world, always receiving and sharing the knowledge with wisdom.