wake-up-neo.com

PHP Spottende Schlussklasse

Ich versuche einen Php final class zu verspotten, da er jedoch als final deklariert ist, erhalte ich diese Fehlermeldung:

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

Gibt es überhaupt etwas, um dieses final-Verhalten nur für meine Unit-Tests zu umgehen, ohne neue Frameworks einzuführen?

25
DanHabib

Da Sie erwähnt haben, dass Sie kein anderes Framework verwenden möchten, haben Sie nur eine Option: uopz

uopz ist eine schwarze magische Erweiterung des Genres Runkit und Scary-Stuff, die bei der QS-Infrastruktur helfen soll.

uopz_flags ist eine Funktion, die die Flags von Funktionen, Methoden und Klassen ändern kann.

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

Wird nachgeben

bool(false)
13
Joe Watkins

Späte Antwort für jemanden, der nach dieser spezifischen Doktrinabfrage sucht.

Doctrine\ORM\Query kann nicht wegen seiner "endgültigen" Deklaration verspottet werden. Wenn Sie sich jedoch den Code der Query-Klasse ansehen, werden Sie feststellen, dass dessen erweiterte AbstractQuery-Klasse keine Probleme macht.

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

Ich schlage vor, dass Sie einen Blick auf das mockery Testing Framework werfen, das eine Problemumgehung für diese auf der Seite beschriebene Situation aufweist: Umgang mit abschließenden Klassen/Methoden :

Sie können einen Proxy-Mock erstellen, indem Sie das instanziierte Objekt übergeben. möchten Mockery in\Mockery :: mock () mockeln, d. h. Mockery generiert dann eine Proxy für das reale Objekt und selektiv Abfragen von Methodenaufrufen für die Zwecke der Festlegung und Erfüllung von Erwartungen.

Als Beispiel erlaubt dies so etwas zu tun:

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

Ich weiß nicht, was Sie tun müssen, aber ich hoffe, diese Hilfe hilft

7
Matteo

2019 Antwort für PHPUnit

Ich sehe, dass Sie PHPUnit verwenden. Sie können Finale aus dieser Antwort umgehen verwenden.

Das Setup ist nur ein bisschen mehr als bootstrap.php. Lesen Sie das "Warum" in Wie verspotte ich Endklassen in PHPUnit .


Hier ist "wie" ↓

2 Schritte

Sie müssen Hook mit Bypass-Aufruf verwenden:

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

Aktualisiere phpunit.xml:

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

Dann können Sie jede Abschlussklasse verspotten :

enter image description here

2
Tomáš Votruba

Ich bin auf das gleiche Problem mit Doctrine\ORM\Query gestoßen. Ich musste den folgenden Code als Unit-Test testen:

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

    $result = $query->getOneOrNullResult();

    ...

}

createQuery gibt Doctrine\ORM\Query-Objekt zurück. Ich konnte Doctrine\ORM\AbstractQuery nicht für meinen Mock verwenden, da er keine setMaxResults-Methode hat, und ich wollte keine anderen Frameworks einführen .. Um die final-Einschränkung der Klasse zu überwinden, verwende ich anonymous classes in PHP 7, die sehr einfach zu erstellen sind. In meiner Testfallklasse habe ich:

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

Dann in meinem 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

Lustiger Weg :) 

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

Ich habe den @Vadym-Ansatz implementiert und ihn aktualisiert. Jetzt benutze ich es erfolgreich zum Testen!

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

Es gibt eine kleine Bibliothek Bypass Finals genau für diesen Zweck. Ausführlich beschrieben von blog post .

Sie müssen dieses Dienstprogramm nur aktivieren, bevor Klassen geladen werden:

DG\BypassFinals::enable();
1
Milo

Wenn Sie eine letzte Klasse verspotten möchten, ist dies ein perfekter Moment, um das Dependency Inversion Prinzip zu verwenden:

Man sollte sich auf Abstraktionen verlassen, nicht auf Konkretionen.

Für die Verspottung bedeutet das: Erstellen Sie eine Abstraktion (Schnittstelle oder abstrakte Klasse) und ordnen Sie sie der endgültigen Klasse zu.

0
Fabian Picone