Mock in PHPUnit - multiple configuration of the same method with different arguments

Sadly this is not possible with the default PHPUnit Mock API.

I can see two options that can get you close to something like this:

Using ->at($x)

$context = $this->getMockBuilder('Context')
   ->getMock();

$context->expects($this->at(0))
   ->method('offsetGet')
   ->with('Matcher')
   ->will($this->returnValue(new Matcher()));

$context->expects($this->at(1))
   ->method('offsetGet')
   ->with('Logger')
   ->will($this->returnValue(new Logger()));

This will work fine but you are testing more than you should (mainly that it gets called with matcher first, and that is an implementation detail).

Also this will fail if you have more than one call to each of of the functions!


Accepting both parameters and using returnCallBack

This is more work but works nicer since you don't depend on the order of the calls:

Working example:

<?php

class FooTest extends PHPUnit_Framework_TestCase {


    public function testX() {

        $context = $this->getMockBuilder('Context')
           ->getMock();

        $context->expects($this->exactly(2))
           ->method('offsetGet')
           ->with($this->logicalOr(
                     $this->equalTo('Matcher'), 
                     $this->equalTo('Logger')
            ))
           ->will($this->returnCallback(
                function($param) {
                    var_dump(func_get_args());
                    // The first arg will be Matcher or Logger
                    // so something like "return new $param" should work here
                }
           ));

        $context->offsetGet("Matcher");
        $context->offsetGet("Logger");


    }

}

class Context {

    public function offsetGet() { echo "org"; }
}

This will output:

/*
$ phpunit footest.php
PHPUnit 3.5.11 by Sebastian Bergmann.

array(1) {
  [0]=>
  string(7) "Matcher"
}
array(1) {
  [0]=>
  string(6) "Logger"
}
.
Time: 0 seconds, Memory: 3.00Mb

OK (1 test, 1 assertion)

I've used $this->exactly(2) in the matcher to show that this does also work with counting the invocations. If you don't need that swapping it out for $this->any() will, of course, work.


As of PHPUnit 3.6, there is $this->returnValueMap() which may be used to return different values depending on the given parameters to the method stub.


You can achieve this with a callback:

class MockTest extends PHPUnit_Framework_TestCase
{
    /**
     * @dataProvider provideExpectedInstance
     */
    public function testMockReturnsInstance($expectedInstance)
    {
        $context = $this->getMock('Context');

        $context->expects($this->any())
           ->method('offsetGet')
           // Accept any of "Matcher" or "Logger" for first argument
           ->with($this->logicalOr(
                $this->equalTo('Matcher'),
                $this->equalTo('Logger')
           ))
           // Return what was passed to offsetGet as a new instance
           ->will($this->returnCallback(
               function($arg1) {
                   return new $arg1;
               }
           ));

       $this->assertInstanceOf(
           $expectedInstance,
           $context->offsetGet($expectedInstance)
       );
    }
    public function provideExpectedInstance()
    {
        return array_chunk(array('Matcher', 'Logger'), 1);
    }
}

Should pass for any "Logger" or "Matcher" arguments passed to the Context Mock's offsetGet method:

F:\Work\code\gordon\sandbox>phpunit NewFileTest.php
PHPUnit 3.5.13 by Sebastian Bergmann.

..

Time: 0 seconds, Memory: 3.25Mb

OK (2 tests, 4 assertions)

As you can see, PHPUnit ran two tests. One for each dataProvider value. And in each of those tests it made the assertion for with() and the one for instanceOf, hence four assertions.


Following on from the answer of @edorian and the comments (@MarijnHuizendveld) regarding ensuring that the method is called with both Matcher and Logger, and not simply twice with either Matcher or Logger, here is an example.

$expectedArguments = array('Matcher', 'Logger');
$context->expects($this->exactly(2))
       ->method('offsetGet')
       ->with($this->logicalOr(
                 $this->equalTo('Matcher'), 
                 $this->equalTo('Logger')
        ))
       ->will($this->returnCallback(
            function($param) use (&$expectedArguments){
                if(($key = array_search($param, $expectedArguments)) !== false) {
                    // remove called argument from list
                    unset($expectedArguments[$key]);
                }
                // The first arg will be Matcher or Logger
                // so something like "return new $param" should work here
            }
       ));

// perform actions...

// check all arguments removed
$this->assertEquals(array(), $expectedArguments, 'Method offsetGet not called with all required arguments');

This is with PHPUnit 3.7.

If the method you are testing doesn't actually return anything, and you simply need to test that it is called with the correct arguments, the same approach applies. For this scenario, I also attempted doing this using a callback function for $this->callback as the argument to the with, rather than returnCallback in the will. This fails, as internally phpunit calls the callback twice in the process of verifying the argument matcher callback. This means that the approach fails as on the second call that argument has already been removed from the expected arguments array. I don't know why phpunit calls it twice (seems an unnecessary waste), and I guess you could work around that by only removing it on the second call, but I wasn't confident enough that this is intended and consistent phpunit behaviour to rely on that occurring.


My 2 cents to the topic: pay attention when using at($x): it means that expected method call will be the ($x+1)th method call on the mock object; it doesn't mean that will be the ($x+1)th call of the expected method. This made me waste some time so I hope it won't with you. Kind regards to everyone.