PHPUnit。 哭泣主义实体经理

许多现代数据库应用程序都使用Doctrine ORM项目。


将数据库与服务一起使用是一种很好的做法。 服务需要进行测试。


要测试服务,可以连接测试数据库,也可以锁定实体管理器和存储库。 使用第一个选项,一切都是清楚的,但是部署数据库来测试服务并非总是有意义。 我们将讨论这个。


例如,采取以下服务:


src /服务/用户/ 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; } } 

我们需要测试其唯一的create()方法。
选择以下情况:


  • 无需引荐来源即可成功创建用户
  • 使用引荐来源网址成功创建用户
  • 错误“已登录”
  • 错误“找不到引荐来源网址”

为了测试服务,我们需要一个实现Doctrine\ORM\EntityManagerInterface接口的对象


选项1.我们使用真实的数据库


我们将为测试编写一个基类,稍后我们将继承该基类。


测试/ 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; } } 

现在,测试可以设置环境变量了。 将它们添加到php部分的phpunit.xml文件中。 我将使用sqlite db


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> 

现在我们将编写一个服务测试


测试/单位/服务/ 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); } } 

确保我们的服务正常运行


 ./vendor/bin/phpunit 

选项2.使用MockBuilder


每次都很难建立数据库。 尤其是phpunit使我们有机会使用mockBuilder即时收集moki。 可以在Symfony文档中找到一个示例


Symfony文档示例
 // 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)); } } 

该选项正在工作,但是存在问题。 您需要清楚地知道代码以什么顺序访问EntityManager方法。
例如,如果开发人员将检查替换为引荐来源网址,然后将检查替换为繁忙登录,则测试将失败。 但是应用程序不是。


我提出了智能调用EntityManager的选项,该选项将其所有数据存储在内存中,并且不使用真实的数据库。


选项3.我们使用MockBuilder将数据存储在内存中。


为了提高灵活性,我们将添加一个环境变量,以便您可以使用真实的数据库。 让我们在phpunit.xml越冬


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

现在修改基类


修改后的测试/ 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); }; } } 

现在,我们可以再次运行测试,并确保我们的服务无需连接数据库即可正常工作。


 ./vendor/bin/phpunit 

Github上可用的源代码

Source: https://habr.com/ru/post/zh-CN452244/


All Articles