Testes de Integração no Symfony com Testcontainers
Joubert RedRat
Posted on August 29, 2024
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!
Posted on August 29, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.