Подтвердить что ты не робот

PHP Mocking Final Class

Я пытаюсь высмеять php final class, но поскольку он объявлен final, я продолжаю получать эту ошибку:

PHPUnit_Framework_Exception: Class "Doctrine\ORM\Query" is declared "final" and cannot be mocked.

Можно ли обойти это поведение final только для моих модульных тестов, не вводя никаких новых фреймворков?

4b9b3361

Ответ 1

Поскольку вы упомянули, что не хотите использовать какие-либо другие рамки, вы оставите только один вариант: 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)

Ответ 2

Поздний отклик для тех, кто ищет ответ на конкретный запрос доктрины.

Вы не можете издеваться над Doctrine\ORM\Query, потому что его "окончательное" объявление, но если вы посмотрите на код класса Query, вы увидите, что его расширяющий класс AbstractQuery и не должно быть никаких проблем, издеваясь над ним.

/** @var \PHPUnit_Framework_MockObject_MockObject|AbstractQuery $queryMock */
$queryMock = $this
    ->getMockBuilder('Doctrine\ORM\AbstractQuery')
    ->disableOriginalConstructor()
    ->setMethods(['getResult'])
    ->getMockForAbstractClass();

Ответ 3

Я предлагаю вам взглянуть на систему тестирования для издевательства, в которой есть обход для этой ситуации, описанной на странице: Работа с окончательными классами/методами:

Вы можете создать прокси-макет, передав созданный объект хотите издеваться над \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());
    }

Я не знаю, что вам нужно делать, но я надеюсь, что эта помощь

Ответ 4

Для этой цели есть небольшая библиотека Bypass Finals. Подробно описано в блоге.

Вам нужно только включить эту утилиту перед загрузкой классов:

DG\BypassFinals::enable();

Ответ 5

Смешной путь:)

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;

Ответ 6

Я реализовал подход @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;
}

Ответ 7

Я наткнулся на ту же проблему с Doctrine\ORM\Query. Мне понадобилось unit test следующий код:

public function someFunction()
{
    // EntityManager was injected in the class 
    $query = $this->entityManager
        ->createQuery('SELECT t FROM Test t')
        ->setMaxResults(1);

    $result = $query->getOneOrNullResult();

    ...

}

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);

    ...

}

Ответ 8

Если вы хотите издеваться над последним классом, это идеальный момент для использования принципа обращения зависимостей:

Нужно полагаться на абстракции, а не на конкреции.

Для насмешек это означает: создать абстракцию (интерфейс или абстрактный класс) и назначить ее конечному классу, а также смоделировать абстракцию.

Ответ 9

2019 ответ для PHPUnit

Я вижу, что вы используете PHPUnit. Вы можете использовать обходной финал из этого ответа.

Настройка чуть больше, чем bootstrap.php. Прочитайте "почему" в разделе Как создавать финальные классы в PHPUnit.


Вот "как" ↓

2 шага

Вам нужно использовать Hook с обходным вызовом:

<?php declare(strict_types=1);

use DG\BypassFinals;
use PHPUnit\Runner\BeforeTestHook;

final class BypassFinalHook implements BeforeTestHook
{
    public function executeBeforeTest(string $test): void
    {
        BypassFinals::enable();
    }
}

Обновление phpunit.xml:

<phpunit bootstrap="vendor/autoload.php">
    <extensions>
        <extension class="Hook\BypassFinalHook"/>
    </extensions>
</phpunit>

Тогда вы можете издеваться над любым последним классом:

enter image description here