web-dev-qa-db-ja.com

PHP最終クラスのモック

私はphpをモックしようとしていますfinal classですが、宣言されているのでfinalこのエラーを受け取り続けます:

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

新しいフレームワークを導入せずに、単体テストだけでこのfinal動作を回避する方法はありますか?

28
DanHabib

あなたは他のフレームワークを使いたくないと言ったので、あなたは1つのオプションだけを残しています: opz

uopzは、runkit-and-scary-stuffジャンルのブラックマジック拡張であり、QAインフラストラクチャの支援を目的としています。

opz_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)
13
Joe Watkins

この特定のdoctrineクエリ模擬回答を探している人への遅い応答。

「最終」宣言なのでDoctrine\ORM\Queryをモックすることはできませんが、Queryクラスのコードを見ると、その拡張AbstractQueryクラスがあり、それをモックする問題はないはずです。

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

このページで説明されているこの状況の回避策がある mockeryテストフレームワーク を確認することをお勧めします。 最終クラス/メソッドの扱い

モックしたいインスタンス化されたオブジェクトを\ 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());
    }

私はあなたが何をする必要があるかわかりませんが、私はこの助けを願っています

7
Matteo

小さなライブラリ Bypass Finals がまさにそのような目的のためにあります。 ブログ投稿 で詳細に説明されています。

クラスをロードする前に、このユーティリティを有効にするだけです。

DG\BypassFinals::enable();
3
Milo

最終クラスをモックしたいとき、それは 依存関係逆転原理 を利用する絶好の機会です:

コンクリートではなく、抽象化に依存するべきです。

モックとは、抽象化(インターフェースまたは抽象クラス)を作成し、それを最終クラスに割り当て、抽象化をモック化することを意味します。

2
Fabian Picone

PHPUnitの2019回答

PHPUnitを使用しているようです。 この回答からファイナルをバイパスする を使用できます。

セットアップはbootstrap.phpより少しだけです。 PHPUnitの最終クラスをモックする方法 の「なぜ」を読んでください。


こちらが「ハウ」↓

2ステップ

あなたはバイパス呼び出しでフックを使用する必要があります:

<?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

2
Tomáš Votruba

Doctrine\ORM\Queryで同じ問題に遭遇しました。次のコードを単体テストする必要がありました。

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

    $result = $query->getOneOrNullResult();

    ...

}

createQueryDoctrine\ORM\Queryオブジェクトを返します。モックにはDoctrine\ORM\AbstractQueryを使用できませんでした。setMaxResultsメソッドがなく、他のフレームワークを導入したくなかったためです。クラスのfinal制限を克服するために、私は anonymous classes in 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);

    ...

}
2
zstate

おかしい方法:)

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

@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;
}
2
Kostiantyn