web-dev-qa-db-fra.com

PHP Finale moqueuse

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

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

Y at-il un moyen de contourner ce comportement final uniquement pour mes tests unitaires sans introduire de nouveaux frameworks?

25
DanHabib

Puisque vous avez mentionné que vous ne souhaitiez utiliser aucun autre cadre, vous ne laissiez qu'une option: uopz

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

uopz_flags est une fonction qui peut modifier les drapeaux de fonctions, méthodes et 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());
?>

Va céder

bool(false)
13
Joe Watkins

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

Vous ne pouvez pas vous moquer de Doctrine\ORM\Query en raison de sa déclaration "finale", mais si vous examinez le code de la classe Query, vous verrez que sa classe AbstractQuery est étendue et qu'il ne devrait y avoir aucun problème à s'en moquer.

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

Je vous suggère de jeter un coup d'œil au framework de test de moquerie qui propose une solution de contournement à cette situation décrite dans la page: Traitement des classes/méthodes finales :

Vous pouvez créer une maquette de proxy en passant l'objet instancié par vous souhaite se moquer de\Mockery :: mock (), c’est-à-dire que Mockery générera alors un Proxy vers l'objet réel et intercepter sélectivement les appels de méthode pour les objectifs d'établissement et de répondre aux attentes.

A titre d'exemple, cela permet 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 cette aide

7
Matteo

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 renvoie l'objet Doctrine\ORM\Query. Je ne pouvais pas utiliser Doctrine\ORM\AbstractQuery pour ma maquette car il n'avait pas de méthode setMaxResults et je ne voulais pas introduire d'autres frameworks . Pour surmonter la restriction final sur la classe que j'utilise classes anonymes dans PHP 7, qui sont très faciles à créer. Dans ma classe 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
zstate

Drôle de 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
Vadym

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
stakantin

Il y a une petite bibliothèque Bypass Finals exactement à cette fin. Décrit en détail par blog post .

Vous devez seulement activer cet utilitaire avant le chargement des classes:

DG\BypassFinals::enable();
1
Milo

Lorsque vous voulez vous moquer d’une dernière classe, c’est le moment idéal pour utiliser Principe de l’inversion de dépendance :

On devrait dépendre d'abstractions, pas de concrétions.

Cela signifie: créer une abstraction (classe d’interface ou abstraite) et l’affecter à la classe finale, puis se moquer de cette abstraction.

0
Fabian Picone