Shikhar Subedi Shikhar Subedi - 3 months ago 14
PHP Question

PHPUnit mocking object that is the same type as the SUT and is a property of the SUT

I have a class called

TestClass
which has a property called
parent
. The
parent
property is the same type as the
TestClass
.
Show below is a portion of the
TestClass
I am trying to test.

class TestClass
{
/**
* @param array $parentIds
* @return ApiLock[]
*/
protected function getParentLocks(array $parentIds)
{
if ($this->getParent()) {
$id = array_pop($parentIds);
return $this->getParent()->getLocks($id, $parentIds);
}

return [];
}

/**
* @param array $ids
* @return ApiLock[]
*/
protected function getLocks($id, array $ids)
{
$locks = $this->getParentLocks($ids);

if ($this->config->getLockName()) {
$locks[] = new ApiLock($this->config->getLockName() . '.' . $id, $this->config->getLockWait());
}

return $locks;
}

}


The method
getLocks
calls the
getParentLocks
method. To test this class, I have mocked
TestClass
object and set as the parent of the SUT. Something similar to this.

$apiLock = $this->getMockBuilder(ApiLock::class)
->disableOriginalConstructor()
->getMock();
$parent = $this->getMockBuilder(TestClass::class)
->disableOriginalConstructor()
->getMock();

$parent->expects($this->any())
->method('getLocks')
->will($this->returnValue([$apiLock]));

$testClass->setParent($parent);


But when i run the tests , the method
setLocks
on the
parent
object will not return the stubbed value but will actually call the
setLocks
method on the parent object and the test fails. May be I am not seeing something obvious.
The stubbed method should be called instead of the real method on the parent object.
Please help me thanks in advance.

Answer

The problem doesn't have anything to do with the parent-child relation.

You should define which methods you want to mock:

    $parent = $this->getMockBuilder(TestClass::class)
        ->setMethods(['setLocks', 'getLocks'])
        ->disableOriginalConstructor()
        ->getMock();

Methods which are not mocked will be called regularly, and your expectations are ignored. You are probably using PHPUnit 4, because in PHPUnit 5 you would see in a warning that you defined an expectation for a method that is not mocked.


Update: It's true that if you don't call setMethods(), all methods should be mocked, but this only applies to public non-abstract methods.

This is the code in PHPUnit_Framework_MockObject_Generator that automatically determines methods to be mocked:

public function getClassMethods($className)
{
    $class   = new ReflectionClass($className);
    $methods = [];
    foreach ($class->getMethods() as $method) {
        if ($method->isPublic() || $method->isAbstract()) {
            $methods[] = $method->getName();
        }
    }
    return $methods;
}

So you can mock protected methods (and also nonexistent methods that would trigger __call()), but other than public methods, you need to specify them explicitly.

Comments