PHPUnit's returnValueMap not yielding expected results

I'm trying to use PHPUnit's returnValueMap() to stub out the results of a read. It isn't yielding the expected results, but an equivalent returnCallback() does. I've made my test case available if you'd like to inspect it yourself.

returnValueMap()

$enterprise = $this->getMock('Enterprise', array('field'));
$enterprise->expects($this->any())
    ->method('field')
    ->will($this->returnValueMap(array(
        array('subscription_id', null),
        array('name', 'Monday Farms')
    )));
$enterprise->subscribe('basic');

Results:

Subscription ID: NULL
Name: NULL

returnCallback()

$enterprise = $this->getMock('Enterprise', array('field'));
$enterprise->expects($this->any())
    ->method('field')
    ->will($this->returnCallback(function ($arg) {
        $map = array(
            'subscription_id' => null,
            'name' => 'Monday Farms'
        );
        return $map[$arg];
    }));
$enterprise->subscribe('basic');

Results:

Subscription ID: NULL
Name: string(12) "Monday Farms"

Enterprise::subscribe

public function subscribe() {
    echo 'Subscription ID: ';
    var_dump($this->field('subscription_id'));
    echo 'Name: ';
    var_dump($this->field('name'));
}

Why doesn't returnValueMap() work as I expect it to? What exactly am I missing?


Solution 1:

I had the same problem and eventually found out that returnValueMap() has to map all parameters of your function, including optional ones, then the desired return value.

Example function from Zend Framework:

public function getParam($key, $default = null)
{
    $key = (string) $key;
    if (isset($this->_params[$key])) {
        return $this->_params[$key];
    }

    return $default;
}

Has to mapped like this:

$request->expects($this->any())
        ->method('getParam')
        ->will($this->returnValueMap(array(array($param, null, $value))));

Without the null in the middle, it won't work.

Solution 2:

I've been hunting down this same problem and eventually out of desperation xdebug-stepped through Framework/MockObject/Stub/ReturnValueMap.php and Framework/MockObject/InvocationMocker.php [in method InvocationMocker::invoke(PHPUnit_Framework_MockObject_Invocation $invocation) ], and I've observed the following points:

  1. Not only must the values in the map-array that you supply be the same as the expected parameters when the stubbed function is called, but they MUST BE OF THE SAME TYPE. This is because inside Framework/MockObject/Stub/ReturnValueMap.php in the ReturnValueMap::invoke() method, the comparison between the supplied parameters and the expected parameters is compared in line 75 as follows:

    if ($invocation->parameters === $map) {
    

    So, the anticipated result of

    $mock->myStubbedMethod( "1", "2" )
    

    using a map-array of

    array(
        array( 1, 1, 500 ),
        array( 1, 2, 700 )
    )
    

    will disappoint. Instead, the result will be a NULL.

  2. On a much more subtle point, you may have stubbed the method using two different mock scenarios (as I did - yes, silly me!)

    So, to elucidate, the first mock-stub could contain

    $mock->expects( $this->any() )
           ->method('myStubbedMethod')
           ->will( $this->returnValue(750) );
    

    and then later, in the same unit-test method, it could contain

    $arrMap = array(
        array( 1, 1, 500 ),
        array( 1, 2, 700 ),
        array( 2, 3, 1500 ),
    );
    $mock->expects( $this->any() )
            ->method('myStubbedMethod')
            ->will( $this->returnValueMap($arrMap) );
    

    When the stubbed-method is invoked, the map-array version will be implemented. This is obvious and self-evident, however while coding in the heat of the moment, and while separating different code behaviour in your mind as you develop, it's easily overlooked.