PHPUnit. Weeping Doctrine Entity Manager

Viele moderne Datenbankanwendungen verwenden das Doctrine ORM- Projekt.


Es wird als bewährte Methode angesehen, die Arbeit mit der Datenbank zu den Diensten zu bringen. Und Dienstleistungen müssen getestet werden.


Zum Testen von Diensten können Sie eine Testdatenbank verbinden oder den Entity Manager und die Repositorys sperren. Mit der ersten Option ist alles klar, aber es ist nicht immer sinnvoll, eine Datenbank zum Testen des Dienstes bereitzustellen. Wir werden darüber reden.


Nehmen Sie zum Beispiel den folgenden Dienst:


src / Service / User / UserService.php
<?php // src/Service/User/UserService.php namespace App\Service\User; use App\Entity\Code; use App\Entity\User; use App\Repository\CodeRepository; use App\Repository\UserRepository; use App\Service\Generator\CodeGenerator; use App\Service\Sender\SenderService; use App\Service\User\Exception\LoginAlreadyExistsException; use App\Service\User\Exception\ReferrerUserNotFoundException; use Doctrine\ORM\EntityManagerInterface; class UserService { /** @var EntityManagerInterface */ private $em; /** @var UserRepository */ private $users; /** @var CodeRepository */ private $codes; /** @var SenderService */ private $sender; /** @var CodeGenerator */ private $generator; public function __construct(EntityManagerInterface $em, SenderService $sender, CodeGenerator $generator) { $this->em = $em; $this->users = $em->getRepository(User::class); $this->codes = $em->getRepository(Code::class); $this->sender = $sender; $this->generator = $generator; } /** * @param string $login * @param string $email * @param string|null $referrerLogin * @return User * @throws LoginAlreadyExistsException * @throws ReferrerUserNotFoundException */ public function create(string $login, string $email, ?string $referrerLogin = null): User { $exists = $this->users->findOneByLogin($login); if ($exists) throw new LoginAlreadyExistsException(); $referrer = null; if ($referrerLogin) { $referrer = $this->users->findOneByLogin($referrerLogin); if (!$referrer) throw new ReferrerUserNotFoundException(); } $user = (new User())->setLogin($login)->setEmail($email)->setReferrer($referrer); $code = (new Code())->setEmail($email)->setCode($this->generator->generate()); $this->sender->sendCode($code); $this->em->persist($user); $this->em->persist($code); $this->em->flush(); return $user; } } 

Wir müssen die einzige Methode create() testen.
Wählen Sie folgende Fälle aus:


  • Erfolgreiche Benutzererstellung ohne Referrer
  • Erfolgreiche Benutzererstellung mit Referrer
  • Fehler "Login bereits vergeben"
  • Fehler "Referrer nicht gefunden"

Zum Testen des Dienstes benötigen wir ein Objekt, das die Schnittstelle Doctrine\ORM\EntityManagerInterface implementiert


Option 1. Wir verwenden eine echte Datenbank


Wir werden eine Basisklasse für Tests schreiben, von der wir später erben werden.


tests / TestCase.php
 <?php // tests/TestCase.php namespace Tests; use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Cache\ArrayCache; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\Driver\AnnotationDriver; use Doctrine\ORM\Tools\SchemaTool; use Doctrine\ORM\Tools\Setup; use PHPUnit\Framework\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { protected function getEntityManager(): EntityManagerInterface { $paths = [ dirname(__DIR__) . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Entity', ]; $cache = new ArrayCache(); $driver = new AnnotationDriver(new AnnotationReader(), $paths); $config = Setup::createAnnotationMetadataConfiguration($paths, false); $config->setMetadataCacheImpl($cache); $config->setQueryCacheImpl($cache); $config->setMetadataDriverImpl($driver); $connection = array( 'driver' => getenv('DB_DRIVER'), 'path' => getenv('DB_PATH'), 'user' => getenv('DB_USER'), 'password' => getenv('DB_PASSWORD'), 'dbname' => getenv('DB_NAME'), ); $em = EntityManager::create($connection, $config); /* *       . *          */ $schema = new SchemaTool($em); $schema->dropSchema($em->getMetadataFactory()->getAllMetadata()); $schema->createSchema($em->getMetadataFactory()->getAllMetadata()); return $em; } } 

Jetzt ist es für die Tests sinnvoll, Umgebungsvariablen festzulegen. Fügen Sie sie der Datei phpunit.xml im Abschnitt php . Ich werde SQLite DB verwenden


phpunit.xml
 <?xml version="1.0" encoding="UTF-8"?> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.1/phpunit.xsd" bootstrap="vendor/autoload.php" executionOrder="depends,defects" forceCoversAnnotation="true" beStrictAboutCoversAnnotation="true" beStrictAboutOutputDuringTests="true" beStrictAboutTodoAnnotatedTests="true" verbose="true" colors="true"> <php> <env name="DB_DRIVER" value="pdo_sqlite" /> <env name="DB_PATH" value="var/db-test.sqlite" /> <env name="DB_USER" value="" /> <env name="DB_PASSWORD" value="" /> <env name="DB_NAME" value="" /> </php> <testsuites> <testsuite name="default"> <directory>tests/Unit</directory> </testsuite> </testsuites> <filter> <whitelist processUncoveredFilesFromWhitelist="true"> <directory suffix=".php">src</directory> </whitelist> </filter> </phpunit> 

Jetzt schreiben wir einen Servicetest


tests / Unit / Service / UserServiceTest.php
 <?php // tests/Unit/Service/UserServiceTest.php namespace Tests\Unit\Service; use App\Entity\Code; use App\Entity\User; use App\Repository\CodeRepository; use App\Repository\UserRepository; use App\Service\Generator\CodeGenerator; use App\Service\Sender\SenderService; use App\Service\User\Exception\LoginAlreadyExistsException; use App\Service\User\Exception\ReferrerUserNotFoundException; use App\Service\User\UserService; use Tests\TestCase; use Doctrine\ORM\EntityManagerInterface; class UserServiceTest extends TestCase { /** * @var UserService */ protected $service; /** * @var EntityManagerInterface */ protected $em; public function setUp(): void { parent::setUp(); $this->em = $this->getEntityManager(); $this->service = new UserService($this->em, new SenderService(), new CodeGenerator()); } /** * @throws LoginAlreadyExistsException * @throws ReferrerUserNotFoundException */ public function testCreateSuccessWithoutReferrer() { //        $login = 'case1'; $email = $login . '@localhost'; $user = $this->service->create($login, $email); // ,       $this->assertInstanceOf(User::class, $user); $this->assertSame($login, $user->getLogin()); $this->assertSame($email, $user->getEmail()); $this->assertFalse($user->isApproved()); // ,      /** @var UserRepository $userRepo */ $userRepo = $this->em->getRepository(User::class); $u = $userRepo->findOneByLogin($login); $this->assertInstanceOf(User::class, $u); $this->assertSame($login, $u->getLogin()); $this->assertSame($email, $u->getEmail()); $this->assertFalse($u->isApproved()); // ,       /** @var CodeRepository $codeRepo */ $codeRepo = $this->em->getRepository(Code::class); $c = $codeRepo->findLastByEmail($email); $this->assertInstanceOf(Code::class, $c); } /** * @throws LoginAlreadyExistsException * @throws ReferrerUserNotFoundException */ public function testCreateSuccessWithReferrer() { //      $referrerLogin = 'referer'; $referrer = new User(); $referrer ->setLogin($referrerLogin) ->setEmail($referrerLogin.'@localhost') ; $this->em->persist($referrer); $this->em->flush(); //        $login = 'case2'; $email = $login . '@localhost'; $user = $this->service->create($login, $email, $referrerLogin); // ,       $this->assertInstanceOf(User::class, $user); $this->assertSame($login, $user->getLogin()); $this->assertSame($email, $user->getEmail()); $this->assertFalse($user->isApproved()); $this->assertSame($referrer, $user->getReferrer()); // ,      /** @var UserRepository $userRepo */ $userRepo = $this->em->getRepository(User::class); $u = $userRepo->findOneByLogin($login); $this->assertInstanceOf(User::class, $u); $this->assertSame($login, $u->getLogin()); $this->assertSame($email, $u->getEmail()); $this->assertFalse($u->isApproved()); // ,       /** @var CodeRepository $codeRepo */ $codeRepo = $this->em->getRepository(Code::class); $c = $codeRepo->findLastByEmail($email); $this->assertInstanceOf(Code::class, $c); } /** * @throws LoginAlreadyExistsException * @throws ReferrerUserNotFoundException */ public function testCreateFailWithNonexistentReferrer() { //   ,     ReferrerUserNotFoundException $this->expectException(ReferrerUserNotFoundException::class); $referrerLogin = 'nonexistent-referer'; $login = 'case3'; $email = $login . '@localhost'; //       $this->service->create($login, $email, $referrerLogin); } /** * @throws LoginAlreadyExistsException * @throws ReferrerUserNotFoundException */ public function testCreateFailWithExistentLogin() { //   ,     LoginAlreadyExistsException $this->expectException(LoginAlreadyExistsException::class); //       $login = 'case4'; $email = $login . '@localhost'; //       ,    $existentUser = new User(); $existentUser ->setLogin($login) ->setEmail($login.'@localhost') ; $this->em->persist($existentUser); $this->em->flush(); //       $this->service->create($login, $email, null); } } 

Stellen Sie sicher, dass unser Service ordnungsgemäß funktioniert


 ./vendor/bin/phpunit 

Option 2. Verwenden von MockBuilder


Es ist schwierig, jedes Mal eine Datenbank zu erstellen. Insbesondere phpunit gibt uns die Möglichkeit, Moki mit mockBuilder im laufenden Betrieb zu sammeln. Ein Beispiel finden Sie in der Symfony-Dokumentation.


Beispiel für eine Symfony-Dokumentation
 // tests/Salary/SalaryCalculatorTest.php namespace App\Tests\Salary; use App\Entity\Employee; use App\Salary\SalaryCalculator; use Doctrine\Common\Persistence\ObjectManager; use Doctrine\Common\Persistence\ObjectRepository; use PHPUnit\Framework\TestCase; class SalaryCalculatorTest extends TestCase { public function testCalculateTotalSalary() { $employee = new Employee(); $employee->setSalary(1000); $employee->setBonus(1100); // Now, mock the repository so it returns the mock of the employee $employeeRepository = $this->createMock(ObjectRepository::class); // use getMock() on PHPUnit 5.3 or below // $employeeRepository = $this->getMock(ObjectRepository::class); $employeeRepository->expects($this->any()) ->method('find') ->willReturn($employee); // Last, mock the EntityManager to return the mock of the repository $objectManager = $this->createMock(ObjectManager::class); // use getMock() on PHPUnit 5.3 or below // $objectManager = $this->getMock(ObjectManager::class); $objectManager->expects($this->any()) ->method('getRepository') ->willReturn($employeeRepository); $salaryCalculator = new SalaryCalculator($objectManager); $this->assertEquals(2100, $salaryCalculator->calculateTotalSalary(1)); } } 

Die Option funktioniert, aber es gibt Probleme. Sie müssen genau wissen, in welcher Reihenfolge der Code auf die EntityManager-Methoden zugreift.
Wenn ein Entwickler beispielsweise die Prüfung auf das Vorhandensein eines Referrers und eine Prüfung auf geschäftige Anmeldung austauscht, wird der Test abgebrochen. Aber die Anwendung ist nicht.


Ich schlage die Option des intelligenten Rauchens EntityManager vor, der alle seine Daten im Speicher speichert und keine echte Datenbank verwendet.


Option 3. Wir verwenden MockBuilder mit Datenspeicherung im Speicher.


Aus Gründen der Flexibilität fügen wir eine Umgebungsvariable hinzu, damit Sie eine echte Datenbank verwenden können. Lassen Sie uns in phpunit.xml überwintern


phpunit.xml ändert sich
 <?xml version="1.0" encoding="UTF-8"?> <!-- ... --> <php> <!-- ... --> <env name="EMULATE_BD" value="1" /> <!-- ... --> </php> <!-- ... --> 

Ändern Sie nun die Basisklasse


modifizierte Tests / TestCase.php
 <?php // tests/TestCase.php namespace Tests; use App\Entity\Code; use App\Entity\User; use App\Repository\CodeRepository; use App\Repository\UserRepository; use Closure; use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Cache\ArrayCache; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\Driver\AnnotationDriver; use Doctrine\ORM\Tools\SchemaTool; use Doctrine\ORM\Tools\Setup; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase as BaseTestCase; use ReflectionClass; abstract class TestCase extends BaseTestCase { /** * @var MockObject[] */ private $_mock = []; private $_data = [ User::class => [], Code::class => [], ]; private $_persist = [ User::class => [], Code::class => [], ]; /** * @var Closure[][] */ private $_fn = []; public function __construct($name = null, array $data = [], $dataName = '') { parent::__construct($name, $data, $dataName); $this->initFn(); } protected function getEntityManager(): EntityManagerInterface { $emulate = (int)getenv('EMULATE_BD'); return $emulate ? $this->getMockEntityManager() : $this->getRealEntityManager(); } protected function getRealEntityManager(): EntityManagerInterface { $paths = [ dirname(__DIR__) . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Entity', ]; $cache = new ArrayCache(); $driver = new AnnotationDriver(new AnnotationReader(), $paths); $config = Setup::createAnnotationMetadataConfiguration($paths, false); $config->setMetadataCacheImpl($cache); $config->setQueryCacheImpl($cache); $config->setMetadataDriverImpl($driver); $connection = array( 'driver' => getenv('DB_DRIVER'), 'path' => getenv('DB_PATH'), 'user' => getenv('DB_USER'), 'password' => getenv('DB_PASSWORD'), 'dbname' => getenv('DB_NAME'), ); $em = EntityManager::create($connection, $config); /* *       . *          */ $schema = new SchemaTool($em); $schema->dropSchema($em->getMetadataFactory()->getAllMetadata()); $schema->createSchema($em->getMetadataFactory()->getAllMetadata()); return $em; } protected function getMockEntityManager(): EntityManagerInterface { return $this->mock(EntityManagerInterface::class); } protected function mock($class) { if (!array_key_exists($class, $this->_mock)) { /* *     */ $mock = $this->getMockBuilder($class) ->disableOriginalConstructor() ->getMock() ; /* *     */ foreach ($this->_fn[$class] as $method => $fn) { $mock /*    */ ->expects($this->any()) /*  $method */ ->method($method) /*  (  )  */ ->with() /*     */ ->will($this->returnCallback($fn)) ; } $this->_mock[$class] = $mock; } return $this->_mock[$class]; } /* *    . *     $fn_[][] */ private function initFn() { /* * EntityManagerInterface::persist($object) -      */ $this->_fn[EntityManagerInterface::class]['persist'] = function ($object) { $entity = get_class($object); switch ($entity) { case User::class: /** @var User $object */ if (!$object->getId()) { $id = count($this->_persist[$entity]) + 1; $reflection = new ReflectionClass($object); $property = $reflection->getProperty('id'); $property->setAccessible(true); $property->setValue($object, $id); } $id = $object->getId(); break; case Code::class: /** @var Code $object */ if (!$object->getId()) { $id = count($this->_persist[$entity]) + 1; $reflection = new ReflectionClass($object); $property = $reflection->getProperty('id'); $property->setAccessible(true); $property->setValue($object, $id); } $id = $object->getId(); break; default: $id = spl_object_hash($object); } $this->_persist[$entity][$id] = $object; }; /* * EntityManagerInterface::flush() -      */ $this->_fn[EntityManagerInterface::class]['flush'] = function () { $this->_data = array_replace_recursive($this->_data, $this->_persist); }; /* * EntityManagerInterface::getRepository($className) -    */ $this->_fn[EntityManagerInterface::class]['getRepository'] = function ($className) { switch ($className) { case User::class: return $this->mock(UserRepository::class); break; case Code::class: return $this->mock(CodeRepository::class); break; } return null; }; /* * UserRepository::findOneByLogin($login) -       */ $this->_fn[UserRepository::class]['findOneByLogin'] = function ($login) { foreach ($this->_data[User::class] as $user) { /** @var User $user */ if ($user->getLogin() == $login) return $user; } return null; }; /* * CodeRepository::findOneByCodeAndEmail -      *        */ $this->_fn[CodeRepository::class]['findOneByCodeAndEmail'] = function ($code, $email) { $result = []; foreach ($this->_data[Code::class] as $c) { /** @var Code $c */ if ($c->getEmail() == $email && $c->getCode() == $code) { $result[$c->getId()] = $c; } } if (!$result) return null; return array_shift($result); }; /* * CodeRepository::findLastByEmail($email) -  ()    *     */ $this->_fn[CodeRepository::class]['findLastByEmail'] = function ($email) { $result = []; foreach ($this->_data[Code::class] as $c) { /** @var Code $c */ if ($c->getEmail() == $email) { $result[$c->getId()] = $c; } } if (!$result) return null; return array_shift($result); }; } } 

Jetzt können wir den Test erneut ausführen und sicherstellen, dass unser Dienst funktioniert, ohne eine Verbindung zur Datenbank herzustellen.


 ./vendor/bin/phpunit 

Quellcode auf Github verfügbar

Source: https://habr.com/ru/post/de452244/


All Articles