PHP se moquant de la classe finale

J'essaie de me moquer d'un php final class mais comme il est déclaré final je continue à recevoir cette erreur:

PHPUnit_Framework_Exception: Class "DoctrineORMQuery" is declared "final" and cannot be mocked.

Existe-t-il de toute façon pour contourner ce comportement final juste pour mes tests unitaires sans introduire de nouveaux frameworks?

23
demandé sur DanHabib 2015-08-25 23:33:55

7 réponses

Puisque vous avez mentionné que vous ne voulez utiliser aucun autre framework, vous ne vous laissez qu'une seule option: uopz

Uopz est une extension de magie noire du genre runkit-and-scary-stuff, destinée à aider avec l'infrastructure QA.

Uopz_flags est une fonction qui peut modifier les drapeaux des fonctions, des méthodes et des classes.

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

Donnera

bool(false)
13
répondu Joe Watkins 2015-10-13 06:07:37

Réponse tardive pour quelqu'un qui cherche cette réponse fictive de requête de doctrine spécifique.

Vous ne pouvez pas vous moquer de Doctrine \ ORM\Query parce que sa déclaration "finale", mais si vous regardez dans le code de la classe Query, vous verrez que sa classe AbstractQuery s'étend et qu'il ne devrait pas y avoir de problèmes.

/** @var \PHPUnit_Framework_MockObject_MockObject|AbstractQuery $queryMock */
$queryMock = $this
    ->getMockBuilder('Doctrine\ORM\AbstractQuery')
    ->disableOriginalConstructor()
    ->setMethods(['getResult'])
    ->getMockForAbstractClass();
8
répondu wormhit 2017-03-01 14:06:13

Je vous suggère de jeter un oeil au cadre de test mockery {[3] } qui a une solution de contournement pour cette situation décrite dans la page: traitant des Classes/méthodes finales :

Vous pouvez créer un mock proxy en passant l'objet instancié vous souhaitez la maquette en \Moquerie::maquette(), c'est à dire la Moquerie va alors générer un Proxy à l'objet réel et intercepte sélectivement les appels de méthode pour les objectifs de l'établissement et de la satisfaction des attentes.

Comme exemple ce permis de faire quelque chose comme ceci:

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

Je ne sais pas ce que vous devez faire, mais, j'espère que cela contribuera

7
répondu Matteo 2015-08-26 08:02:48

Drôle façon :)

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;
2
répondu Vadym 2017-01-02 17:53:50

J'ai implémenté l'approche @Vadym et l'ai mise à jour. Maintenant, je l'utilise pour tester avec succès!

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;
}
2
répondu stakantin 2017-02-03 08:11:29

Je suis tombé sur le même problème avec Doctrine\ORM\Query. J'avais besoin de tester le code suivant:

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

    $result = $query->getOneOrNullResult();

    ...

}

createQuery retourne Doctrine\ORM\Query objet. Je ne pouvais pas utiliser Doctrine\ORM\AbstractQuery pour mon mock car il n'a pas de méthode setMaxResults et je ne voulais pas introduire d'autres frameworks. Pour surmonter la restriction final sur la classe, j'utilise classes anonymes {[13] } en PHP 7, qui sont super faciles à créer. Dans ma classe de cas de test, j'ai:

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

, Puis dans mon test:

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

    ...

}
2
répondu zstate 2017-12-26 22:16:26

Il y a une petite bibliothèque Contournement de Finale exactement pour cette raison. Décrit en détail par blog.

Vous n'avez Qu'à activer cet utilitaire avant le chargement des classes:

DG\BypassFinals::enable();
1
répondu Milo 2018-06-11 11:08:07