Disclaimer: I am not a divine entity. What I say is not an absolute truth. Don't be afraid to question even the world, because it could be wrong, not you.
Today it's no secret to anyone the importance of automated tests to maintain the quality and integrity of your software and we usually talk a lot about unit tests, however, today, we will focus more on integration tests in the Symfony Framework.
Okay okay! If you don't have the patience to read this article, I have a test project implementing this article at the link below.
https://github.com/joubertredrat/symfony-testcontainers
Today, the Symfony Framework is one of the most mature frameworks in the PHP universe and it has several well-implemented solutions, such as for integration tests, for example. However, personally I always found that although it is easy to do the integration tests themselves, providing external dependencies for the tests was not always so easy, like databases for example.
Even with the emergence of Docker, I still realized the need to provision external dependencies in some way for testing, however, there is a very interesting solution that can make this step much easier, Testcontainers.
Testcontainers is an open source framework that allows you to more easily provision any external dependency you need using Docker, such as a database, message broker, caching systems, or practically any container dependency.
The big difference between Testcontainers in relation to Docker compose or any other form of container provisioning is that you can program container provisioning, and today it already has support for Golang, Java, .NET, Node.js, Python , Rust, several other languages, and of course, PHP!
My first contact with Testcontainers was with a Golang project and I really liked the ease of provisioning a MongoDB container to carry out my tests in the repositories and after that, I decided to do the same in a personal project that I have in PHP using the Symfony Framework.
One of the great advantages of Symfony is precisely the support for tests in PHPUnit by providing a fully functional Kernel to perform the necessary bootstrap for tests.
Although Testcontainers supports PHP, the implementation is newer and you can check it out at https://github.com/testcontainers/testcontainers-php.
Below we have an implementation of a MySQL 8.0 container, which is the external dependency of this project, in addition to booting the Symfony Kernel, creating the database and 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(); } }
With this, we have the base class for the classes that will actually execute the tests, as in the example below.
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); } }
When running the test suite, you will notice a delay in finishing the tests, however, this is normal, because during this process, Testcontainers is provisioning the container that you defined to use in the tests.
Finally, with this ease, you can even try to do crazy things, like 100% coverage. Don't believe it? You can see it yourself at https://joubertredrat.github.io/symfony-testcontainers.
So that's it, until next time!
The above is the detailed content of Integration Tests in Symfony with Testcontainers. For more information, please follow other related articles on the PHP Chinese website!