PHP模拟最终类

时间:2015-08-25 20:33:56

标签: php unit-testing doctrine-orm mocking phpunit

我正在尝试模拟一个php #box但是因为它被声明为final class我一直收到这个错误:

final

在没有引入任何新框架的情况下,有没有为我的单元测试解决这个PHPUnit_Framework_Exception: Class "Doctrine\ORM\Query" is declared "final" and cannot be mocked.行为?

9 个答案:

答案 0 :(得分:12)

由于您提到您不想使用任何其他框架,因此您只留下一个选项:uopz

uopz是runkit-and-scary-stuff类型的黑魔术扩展,旨在帮助QA基础设施。

uopz_flags是一个可以修改函数,方法和类的标志的函数。

bool(false)

将产生

;(function($) {
   $.fn.fixMe = function() {
      return this.each(function() {
         var $this = $(this),
            $t_fixed;
         function init() {
            $this.wrap('<div class="container" />');
            $t_fixed = $this.clone();
            $t_fixed.find("tbody").remove().end().addClass("fixed").insertBefore($this);
            resizeFixed();
         }
         function resizeFixed() {
            $t_fixed.find("th").each(function(index) {
               $(this).css("width",$this.find("th").eq(index).outerWidth()+"px");
            });
         }
         function scrollFixed() {
            var offset = $(this).scrollTop(),
            tableOffsetTop = $this.offset().top,
            tableOffsetBottom = tableOffsetTop + $this.height() - $this.find("thead").height();
            if(offset < tableOffsetTop || offset > tableOffsetBottom)
               $t_fixed.hide();
            else if(offset >= tableOffsetTop && offset <= tableOffsetBottom && $t_fixed.is(":hidden"))
               $t_fixed.show();
         }
         $(window).resize(resizeFixed);
         $(window).scroll(scrollFixed);
         init();
      });
   };
})(jQuery);

$(document).ready(function(){
   $("table").fixMe();
   $(".up").click(function() {
      $('html, body').animate({
      scrollTop: 0
   }, 2000);
 });
});

答案 1 :(得分:11)

正在寻找此特定学说查询模拟答案的人的延迟回复。

你不能模仿Doctrine \ ORM \ Query,因为它的“最终”声明,但如果你查看Query类代码,那么你会看到它扩展的AbstractQuery类,并且不应该有任何问题嘲笑它。

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

答案 2 :(得分:7)

我建议您查看mockery testing framework针对此情况的解决方法:Dealing with Final Classes/Methods

  

您可以通过传递实例化对象来创建代理模拟   希望模拟成\ Mockery :: mock(),即Mockery将生成一个   代理到真实对象并有选择地拦截方法调用   设定和满足期望的目的。

例如,这允许做这样的事情:

>>> print "\n".join("%-16s : %s" % (e, getattr(function, e)) for e in dir(function) if not e.startswith("_") and e != "func_globals")
func_closure     : (<cell at 0x2b919f6bc050: list object at [...]>,)
func_code        : <code object inner at [...], file "<stdin>", line 4>
func_defaults    : None
func_dict        : {}
func_doc         : None
func_name        : inner

我不知道你需要做什么,但我希望这有帮助

答案 3 :(得分:2)

有趣的方式:)

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;

答案 4 :(得分:2)

我实施了@Vadym方法并对其进行了更新。现在我用它来成功测试!

MenuOption

答案 5 :(得分:2)

我偶然发现了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();

    ...

}

createQuery返回Doctrine\ORM\Query个对象。我无法使用Doctrine\ORM\AbstractQuery进行模拟,因为它没有setMaxResults方法,我也不想引入任何其他框架。 为了克服对类的final限制,我在PHP 7中使用anonymous classes,这非常容易创建。在我的测试用例类中,我有:

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

    ...

}

答案 6 :(得分:2)

有一个小型库Bypass Finals正是出于此目的。由blog post详细描述。

只有您需要做的是在加载类之前启用此实用程序:

DG\BypassFinals::enable();

答案 7 :(得分:1)

当您要模拟最终课程时,这是使用Dependency inversion principle的绝佳时机:

  

应该依靠抽象而不是凝结。

进行模拟意味着:创建一个抽象(接口或抽象类)并将其分配给最终类,然后模拟该抽象。

答案 8 :(得分:0)

2019年PHPUnit答案

我看到您正在使用PHPUnit。您可以使用bypass finals from this answer

设置仅比bootstrap.php多一点。阅读How to Mock Final Classes in PHPUnit中的“为什么”。


这是“如何”↓

2个步骤

您需要将Hook与旁路呼叫配合使用:

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