Cara menggunakan php uopz

Saya mencoba untuk mengejek php final class tetapi karena dinyatakan final saya terus menerima kesalahan ini:

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

Apakah ada cara untuk mengatasi perilaku final ini hanya untuk pengujian unit saya tanpa memperkenalkan kerangka kerja baru?

Karena Anda menyebutkan Anda tidak ingin menggunakan kerangka kerja lain, Anda hanya meninggalkan satu opsi: uopz

uopz adalah ekstensi ilmu hitam dari genre runkit-and-scary-stuff, yang dimaksudkan untuk membantu infrastruktur QA.

uopz_flags adalah fungsi yang dapat memodifikasi flag fungsi, metode, dan kelas.

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

Akan menghasilkan

bool(false)

Respons telat bagi seseorang yang mencari jawaban tiruan ajaran tertentu ini.

Anda tidak dapat mengejek Doctrine\ORM\Query karena deklarasi "final", tetapi jika Anda melihat ke dalam kode kelas Query maka Anda akan melihat bahwa itu memperluas kelas AbstractQuery dan seharusnya tidak ada masalah mengejeknya.

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

Saya sarankan Anda untuk melihat kerangka pengujian ejekan yang memiliki solusi untuk situasi ini dijelaskan di halaman: Berurusan dengan Kelas/Metode Final :

Anda dapat membuat proksi tiruan dengan meneruskan objek instantiated yang Anda Ingin tiru ke\Mockery :: mock (), yaitu Mockery kemudian akan menghasilkan Proxy ke objek nyata dan secara selektif mencegat panggilan metode untuk tujuan menetapkan dan memenuhi harapan.

Sebagai contoh, izin ini untuk melakukan hal seperti ini:

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

Saya tidak tahu apa yang perlu Anda lakukan tetapi, saya harap bantuan ini

Saya menemukan masalah yang sama dengan Doctrine\ORM\Query. Saya perlu menguji unit kode berikut:

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

    $result = $query->getOneOrNullResult();

    ...

}

createQuery mengembalikan objek Doctrine\ORM\Query. Saya tidak dapat menggunakan Doctrine\ORM\AbstractQuery untuk tiruan saya karena tidak memiliki metode setMaxResults dan saya tidak ingin memperkenalkan kerangka kerja lain. Untuk mengatasi pembatasan final pada kelas saya menggunakan kelas anonim di PHP 7, yang super mudah dibuat. Di kelas ujian saya, saya punya:

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

Kemudian dalam pengujian saya:

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

    ...

}

Cara yang lucu :) 

PHP7.1, PHPUnit 5.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;

Saya telah menerapkan pendekatan @Vadym dan memperbaruinya. Sekarang saya menggunakannya untuk pengujian berhasil!

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

Ada perpustakaan kecil Bypass Finals tepatnya untuk tujuan tersebut. Dijelaskan secara rinci oleh posting blog .

Yang harus Anda lakukan hanyalah mengaktifkan utilitas ini sebelum kelas dimuat:

DG\BypassFinals::enable();

Ketika Anda ingin mengejek kelas akhir, momen yang tepat untuk memanfaatkan Prinsip inversi ketergantungan :

Orang harus bergantung pada abstraksi, bukan konkret.

Untuk mengejek artinya: Membuat abstraksi (antarmuka atau kelas abstrak) dan menetapkannya ke kelas akhir, dan mengejek abstraksi.