174

I've got a PHPUnit mock object that returns 'return value' no matter what its arguments:

// From inside a test...
$mock = $this->getMock('myObject', 'methodToMock');
$mock->expects($this->any))
     ->method('methodToMock')
     ->will($this->returnValue('return value'));

What I want to be able to do is return a different value based on the arguments passed to the mock method. I've tried something like:

$mock = $this->getMock('myObject', 'methodToMock');

// methodToMock('one')
$mock->expects($this->any))
     ->method('methodToMock')
     ->with($this->equalTo('one'))
     ->will($this->returnValue('method called with argument "one"'));

// methodToMock('two')
$mock->expects($this->any))
     ->method('methodToMock')
     ->with($this->equalTo('two'))
     ->will($this->returnValue('method called with argument "two"'));

But this causes PHPUnit to complain if the mock isn't called with the argument 'two', so I assume that the definition of methodToMock('two') overwrites the definition of the first.

So my question is: Is there any way to get a PHPUnit mock object to return a different value based on its arguments? And if so, how?

11 Answers 11

144

Use a callback. e.g. (straight from PHPUnit documentation):

<?php
class StubTest extends PHPUnit_Framework_TestCase
{
    public function testReturnCallbackStub()
    {
        $stub = $this->getMock(
          'SomeClass', array('doSomething')
        );
 
        $stub->expects($this->any())
             ->method('doSomething')
             ->willReturnCallback('callback');
 
        // $stub->doSomething() returns callback(...)
    }
}
 
function callback() {
    $args = func_get_args();
    // ...
}
?>

Do whatever processing you want in the callback() and return the result based on your $args as appropriate.

Sign up to request clarification or add additional context in comments.

7 Comments

Can you provide a link to the documentation? I can't seem to find it with "the Google"
Note that you can use a method as a callback by passing an array, e.g. $this->returnCallback(array('MyClassTest','myCallback')).
It should also be possible to pass directly a closure to it
This should be used only in rare cases. I would suggest using returnValueMap instead as it does not require custom logic to be written in the callback.
I can't thank you enough. Also, with PHP versions > 5.4, you can use an anonymous function as the callback. $this->returnCallback(function() { // ... })
|
139

From the latest phpUnit docs: "Sometimes a stubbed method should return different values depending on a predefined list of arguments. You can use returnValueMap() to create a map that associates arguments with corresponding return values."

$mock->expects($this->any())
    ->method('getConfigValue')
    ->will(
        $this->returnValueMap(
            array(
                array('firstparam', 'secondparam', 'retval'),
                array('modes', 'foo', array('Array', 'of', 'modes'))
            )
        )
    );

Comments

53

I had a similar problem (although slightly different... I didn't need different return value based on arguments, but had to test to ensure 2 sets of arguments were being passed to the same function). I stumbled upon using something like this:

$mock = $this->getMock();
$mock->expects($this->at(0))
    ->method('foo')
    ->with(...)
    ->will($this->returnValue(...));

$mock->expects($this->at(1))
    ->method('foo')
    ->with(...)
    ->will($this->returnValue(...));

It's not perfect, since it requires that the order of the 2 calls to foo() is known, but in practice this probably isn't too bad.

1 Comment

The at() matcher has been deprecated. It will be removed in PHPUnit 10. Please refactor your test to not rely on the order in which methods are invoked.
29

You would probably want to do a callback in a OOP fashion:

<?php
class StubTest extends PHPUnit_Framework_TestCase
{
    public function testReturnAction()
    {
        $object = $this->getMock('class_name', array('method_to_mock'));
        $object->expects($this->any())
            ->method('method_to_mock')
            ->will($this->returnCallback(array($this, 'returnTestDataCallback')));

        $object->returnAction('param1');
        // assert what param1 should return here

        $object->returnAction('param2');
        // assert what param2 should return here
    }
    
    public function returnTestDataCallback()
    {
        $args = func_get_args();

        // process $args[0] here and return the data you want to mock
        return 'The parameter was ' . $args[0];
    }
}
?>

Comments

28

It is not exactly what you ask, but in some cases it can help:

$mock->expects( $this->any() ) )
 ->method( 'methodToMock' )
 ->will( $this->onConsecutiveCalls( 'one', 'two' ) );

onConsecutiveCalls - returns a list of values in the specified order

Comments

14

Pass two level array, where each element is an array of:

  • first are method parameters, and last is return value.

example:

->willReturnMap([
    ['firstArg', 'secondArg', 'returnValue']
])

1 Comment

the best answer! note: params with default values should also be specified
5

You also can return the argument as follows:

$stub = $this->getMock(
  'SomeClass', array('doSomething')
);

$stub->expects($this->any())
     ->method('doSomething')
     ->will($this->returnArgument(0));

As you can see in the Mocking documentation, the method returnValue($index) allows to return the given argument.

1 Comment

I think Stub vs Mock is already confusing topic for many people. You make it even more confusing by doing $stub = $this->getMock(
0

Do you mean something like this?

public function TestSomeCondition($condition){
  $mockObj = $this->getMockObject();
  $mockObj->setReturnValue('yourMethod',$condition);
}

1 Comment

I think that's SimpleTest code, not PHPUnit. But no, it isn't what I want to achieve. Say I had a mock object that returned a word for a given number. My mock method would need to return "one" when called with 1, "two" when called with 2 etc. $
0

I had a similar problem which I couldn't work out as well (there's surprisingly little information about for PHPUnit). In my case, I just made each test separate test - known input and known output. I realised that I didn't need to make a jack-of-all-trades mock object, I only needed a specific one for a specific test, and thus I separated the tests out and can test individual aspects of my code as a separate unit. I'm not sure if this might be applicable to you or not, but that's down to what you need to test.

1 Comment

Unfortunately that wouldn't work in my situation. The mock is being passed into a method I'm testing, and the test method calls the mocked method with different arguments. It's interesting to know that you couldn't solve the problem though. It sounds like this could be a PHPUnit limitation.
-1
$this->BusinessMock = $this->createMock('AppBundle\Entity\Business');

    public function testBusiness()
    {
        /*
            onConcecutiveCalls : Whether you want that the Stub returns differents values when it will be called .
        */
        $this->BusinessMock ->method('getEmployees')
                                ->will($this->onConsecutiveCalls(
                                            $this->returnArgument(0),
                                            $this->returnValue('employee')                                      
                                            )
                                      );
        // first call

        $this->assertInstanceOf( //$this->returnArgument(0),
                'argument',
                $this->BusinessMock->getEmployees()
                );
       // second call


        $this->assertEquals('employee',$this->BusinessMock->getEmployees()) 
      //$this->returnValue('employee'),


    }

Comments

-2

Try :

->with($this->equalTo('one'),$this->equalTo('two))->will($this->returnValue('return value'));

1 Comment

This answer does not apply to the original question, but it details a similar problem I had: verify that a certain set of parameters is provided. PHPUnit's with() accepts multiple arguments, one matcher for each parameter.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.