PHP Mocking Final Class

by DanHabib   Last Updated February 11, 2019 09:26 AM

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?



Answers 8


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

Matteo
Matteo
August 26, 2015 08:02 AM

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)
Joe Watkins
Joe Watkins
October 13, 2015 06:07 AM

Funny way :)

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;
Vadym
Vadym
January 02, 2017 17:53 PM

I've implemented @Vadym approach and updated it. Now I use it for testing successfully!

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;
}
stakantin
stakantin
February 03, 2017 08:11 AM

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();
wormhit
wormhit
March 01, 2017 14:06 PM

I stumbled upon the same problem with Doctrine\ORM\Query. I needed to unit test the following code:

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

    $result = $query->getOneOrNullResult();

    ...

}

createQuery returns Doctrine\ORM\Query object. I couldn't use Doctrine\ORM\AbstractQuery for my mock because it doesn't have setMaxResults method and I didn't want to introduce any other frameworks. To overcome the final restriction on the class I use anonymous classes in PHP 7, which are super easy to create. In my test case class I have:

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

Then in my 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);

    ...

}
zstate
zstate
December 26, 2017 22:16 PM

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();
Milo
Milo
June 11, 2018 11:02 AM

When you want to mock a final class, its a perfect moment to make use of Dependency inversion principle:

One should depend upon abstractions, not concretions.

That means: Assign an abstraction (implement an interface or extend an abstract class) on the final class and mock the abstraction.

Fabian Picone
Fabian Picone
February 11, 2019 09:25 AM

Related Questions


PHPUnit cannot mock __get function on mock object

Updated April 03, 2017 06:26 AM

Unit Test for Parent Method Call

Updated October 29, 2017 08:26 AM

Mock Mailer in Symfony service

Updated February 23, 2017 12:26 PM

Mocking Twig_Environment

Updated November 06, 2017 21:26 PM