Я пытаюсь высмеять final class
php, но поскольку он объявлен final
я продолжаю получать эту ошибку:
PHPUnit_Framework_Exception: Class "Doctrine\ORM\Query" is declared "final" and cannot be mocked.
Нужно ли вообще обойти это final
поведение только для моих модульных тестов без введения каких-либо новых фреймворков?
Поскольку вы упомянули, что не хотите использовать какие-либо другие рамки, вы оставите только один вариант: uopz
uopz – это черное магическое расширение жанра runkit-and-scary-stuff, предназначенного для поддержки инфраструктуры QA.
uopz_flags – это функция, которая может изменять флаги функций, методов и классов.
<?php final class Test {} /** ZEND_ACC_CLASS is defined as 0, just looks nicer ... **/ uopz_flags(Test::class, null, ZEND_ACC_CLASS); $reflector = new ReflectionClass(Test::class); var_dump($reflector->isFinal()); ?>
Будет давать
bool(false)
Я предлагаю вам взглянуть на фреймворк тестирования, в котором есть обходной путь для этой ситуации, описанной на странице: Работа с конечными классами / методами :
Вы можете создать прокси-макет, передав экземпляр объекта, который вы хотите высмеять в \ Mockery :: mock (), то есть Mockery затем сгенерирует прокси-сервер для реального объекта и выборочно перехватывает вызовы методов для целей установки и удовлетворения ожиданий.
В качестве примера это позволяет сделать что-то вроде этого:
class MockFinalClassTest extends \PHPUnit_Framework_TestCase { public function testMock() { $em = \Mockery::mock("Doctrine\ORM\EntityManager"); $query = new Doctrine\ORM\Query($em); $proxy = \Mockery::mock($query); $this->assertNotNull($proxy); $proxy->setMaxResults(4); $this->assertEquals(4, $query->getMaxResults()); }
-class MockFinalClassTest extends \PHPUnit_Framework_TestCase { public function testMock() { $em = \Mockery::mock("Doctrine\ORM\EntityManager"); $query = new Doctrine\ORM\Query($em); $proxy = \Mockery::mock($query); $this->assertNotNull($proxy); $proxy->setMaxResults(4); $this->assertEquals(4, $query->getMaxResults()); }
Я не знаю, что вам нужно сделать, но я надеюсь, что эта помощь
Поздний ответ для тех, кто ищет ответ на конкретный запрос доктрины.
Вы не можете издеваться над Doctrine \ ORM \ Query, потому что его «окончательное» объявление, но если вы посмотрите на код класса Query, вы увидите, что его расширяющий класс AbstractQuery и не должно быть никаких проблем, издеваясь над ним.
/** @var \PHPUnit_Framework_MockObject_MockObject|AbstractQuery $queryMock */ $queryMock = $this ->getMockBuilder('Doctrine\ORM\AbstractQuery') ->disableOriginalConstructor() ->setMethods(['getResult']) ->getMockForAbstractClass();
Смешной путь 🙂
PHP7.1, PHPUnit5.7
<?php use Doctrine\ORM\Query; //... $originalQuery = new Query($em); $allOriginalMethods = get_class_methods($originalQuery); // some "unmockable" methods will be skipped $skipMethods = [ '__construct', 'staticProxyConstructor', '__get', '__set', '__isset', '__unset', '__clone', '__sleep', '__wakeup', 'setProxyInitializer', 'getProxyInitializer', 'initializeProxy', 'isProxyInitialized', 'getWrappedValueHolderValue', 'create', ]; // list of all methods of Query object $originalMethods = []; foreach ($allOriginalMethods as $method) { if (!in_array($method, $skipMethods)) { $originalMethods[] = $method; } } // Very dummy mock $queryMock = $this ->getMockBuilder(\stdClass::class) ->setMethods($originalMethods) ->getMock() ; foreach ($originalMethods as $method) { // skip "unmockable" if (in_array($method, $skipMethods)) { continue; } // mock methods you need to be mocked if ('getResult' == $method) { $queryMock->expects($this->any()) ->method($method) ->will($this->returnCallback( function (...$args) { return []; } ) ); continue; } // make proxy call to rest of the methods $queryMock->expects($this->any()) ->method($method) ->will($this->returnCallback( function (...$args) use ($originalQuery, $method, $queryMock) { $ret = call_user_func_array([$originalQuery, $method], $args); // mocking "return $this;" from inside $originalQuery if (is_object($ret) && get_class($ret) == get_class($originalQuery)) { if (spl_object_hash($originalQuery) == spl_object_hash($ret)) { return $queryMock; } throw new \Exception( sprintf( 'Object [%s] of class [%s] returned clone of itself from method [%s]. Not supported.', spl_object_hash($originalQuery), get_class($originalQuery), $method ) ); } return $ret; } )) ; } return $queryMock;
Я реализовал подход @Vadym и обновил его. Теперь я использую его для тестирования!
protected function getFinalMock($originalObject) { if (gettype($originalObject) !== 'object') { throw new \Exception('Argument must be an object'); } $allOriginalMethods = get_class_methods($originalObject); // some "unmockable" methods will be skipped $skipMethods = [ '__construct', 'staticProxyConstructor', '__get', '__set', '__isset', '__unset', '__clone', '__sleep', '__wakeup', 'setProxyInitializer', 'getProxyInitializer', 'initializeProxy', 'isProxyInitialized', 'getWrappedValueHolderValue', 'create', ]; // list of all methods of Query object $originalMethods = []; foreach ($allOriginalMethods as $method) { if (!in_array($method, $skipMethods)) { $originalMethods[] = $method; } } $reflection = new \ReflectionClass($originalObject); $parentClass = $reflection->getParentClass()->name; // Very dummy mock $mock = $this ->getMockBuilder($parentClass) ->disableOriginalConstructor() ->setMethods($originalMethods) ->getMock(); foreach ($originalMethods as $method) { // skip "unmockable" if (in_array($method, $skipMethods)) { continue; } // make proxy call to rest of the methods $mock ->expects($this->any()) ->method($method) ->will($this->returnCallback( function (...$args) use ($originalObject, $method, $mock) { $ret = call_user_func_array([$originalObject, $method], $args); // mocking "return $this;" from inside $originalQuery if (is_object($ret) && get_class($ret) == get_class($originalObject)) { if (spl_object_hash($originalObject) == spl_object_hash($ret)) { return $mock; } throw new \Exception( sprintf( 'Object [%s] of class [%s] returned clone of itself from method [%s]. Not supported.', spl_object_hash($originalObject), get_class($originalObject), $method ) ); } return $ret; } )); } return $mock; }
Я наткнулся на ту же проблему с Doctrine\ORM\Query
. Мне нужно было протестировать следующий код:
public function someFunction() { // EntityManager was injected in the class $query = $this->entityManager ->createQuery('SELECT t FROM Test t') ->setMaxResults(1); $result = $query->getOneOrNullResult(); ... }
createQuery
возвращает createQuery
Doctrine\ORM\Query
. Я не мог использовать Doctrine\ORM\AbstractQuery
для моего макета, потому что у него нет метода setMaxResults
и я не хотел вводить какие-либо другие фреймворки. Чтобы преодолеть final
ограничение для класса, я использую анонимные классы в PHP 7, которые очень легко создавать. В моем классе тестов у меня есть:
private function getMockDoctrineQuery($result) { $query = new class($result) extends AbstractQuery { private $result; /** * Overriding original constructor. */ public function __construct($result) { $this->result = $result; } /** * Overriding setMaxResults */ public function setMaxResults($maxResults) { return $this; } /** * Overriding getOneOrNullResult */ public function getOneOrNullResult($hydrationMode = null) { return $this->result; } /** * Defining blank abstract method to fulfill AbstractQuery */ public function getSQL(){} /** * Defining blank abstract method to fulfill AbstractQuery */ protected function _doExecute(){} }; return $query; }
Тогда в моем тесте:
public function testSomeFunction() { // Mocking doctrine Query object $result = new \stdClass; $mockQuery = $this->getMockQuery($result); // Mocking EntityManager $entityManager = $this->getMockBuilder(EntityManagerInterface::class)->getMock(); $entityManager->method('createQuery')->willReturn($mockQuery); ... }