PHP Mocking Final Class

22,757

Solution 1

Since you mentioned you don't want to use any other framework, you are only leaving yourself one option: uopz

uopz is a black magic extension of the runkit-and-scary-stuff genre, intended to help with QA infrastructure.

uopz_flags is a function that can modify the flags of functions, methods and 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());
?>

Will yield

bool(false)

Solution 2

Late response for someone who is looking for this specific doctrine query mock answer.

You can not mock Doctrine\ORM\Query because its "final" declaration, but if you look into Query class code then you will see that its extending AbstractQuery class and there should not be any problems mocking it.

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

Solution 3

I suggest you to take a look at the mockery testing framework that have a workaround for this situation described in the page: Dealing with Final Classes/Methods:

You can create a proxy mock by passing the instantiated object you wish to mock into \Mockery::mock(), i.e. Mockery will then generate a Proxy to the real object and selectively intercept method calls for the purposes of setting and meeting expectations.

As example this permit to do something like this:

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

I don't know what you need to do but, i hope this help

Solution 4

There is a small library Bypass Finals exactly for such purpose. Described in detail by blog post.

Only you have to do is enable this utility before classes are loaded:

DG\BypassFinals::enable();

Solution 5

2019 answer for PHPUnit

I see you're using PHPUnit. You can use bypass finals from this answer.

The setup is just a bit more than bootstrap.php. Read "why" in How to Mock Final Classes in PHPUnit.


Here is "how" ↓

2 Steps

You need to use Hook with bypass call:

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

Update phpunit.xml:

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

Then you can mock any final class:

enter image description here

Share:
22,757
DanHabib
Author by

DanHabib

React/iOS in the front, PHP/Python/Aerospike/Mysql in the back Enjoy Traveling, Reading, and Running

Updated on March 30, 2020

Comments

  • DanHabib
    DanHabib about 4 years

    I am attempting to mock a php final class but since it is declared final I keep receiving this error:

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

    Is there anyway to get around this final behavior just for my unit tests without introducing any new frameworks?

  • b01
    b01 almost 7 years
    This works for a class marked final that extend an abstract or implement an interface. If the class itself is being defined final then you'll have to use one of the other work-a-rounds.
  • BentCoder
    BentCoder about 6 years
    Much cleaner compared to all the others. +1
  • Geoff Maddock
    Geoff Maddock over 5 years
    This solution looks pretty good, but I'm trying to get it to work with codeception, and finding this error when I call 'getResult': Trying to configure method "getResult" which cannot be configured because it does not exist, has not been specified, is final, or is static
  • Geoff Maddock
    Geoff Maddock over 5 years
    After a little more digging - using 'execute' rather than 'getResult' allowed this to work!
  • afilina
    afilina over 5 years
    Is there any alternative for php 5.5?
  • Milo
    Milo over 5 years
    The tool itself works with PHP 5.4 today. You can download the BypassFinals.php, the only required file, and include it.
  • fabpico
    fabpico about 5 years
    When the mocking class has final dependencies that has final dependencies its getting a mess. It seems only useful for final classes that has no dependencies.
  • pstryk
    pstryk almost 5 years
    OK, but if the final class belongs to third party library, you cannot just simply edit it. What I found out however: if you create an interface in your code, and the final class just happens to implement it - i.e. it has the same method signatures but no implements keyword, it will still work :)
  • fabpico
    fabpico almost 5 years
    In this case you should adapt. Create an new interface in your project that will be your mockable dependency instead of the 3rd party dependency. The implementation of the new interface will then inject the unmockable 3rd party dependency. The implementation should not have any logic, only act as gateway to the 3rd party class methods, that is not worth to unit test.
  • pstryk
    pstryk almost 5 years
    Well, I must retract what I just said. The tests are passing, yes, but just because I injected what I wanted and made assertions that rely on it. However, application is not working anymore, because of error: method should return instance of my interface, but it just got instance of said final class instead. So it looks the implements keyword is necessary. In this situation your proposed solution seems to be the only sensible way. Thank you :)
  • Alexander
    Alexander almost 5 years
    DG\BypassFinals utilizes stream_wrapper_register. This could prevent tools (f.e. Infection) relying on the same mechanism from working correctly.
  • k0pernikus
    k0pernikus over 3 years
    There's a more indepth article available here: tomasvotruba.com/blog/2019/03/28/…