72

I am writing a unit test for a method using PHPUnit. The method I am testing makes a call to the same method on the same object 3 times but with different sets of arguments. My question is similar to the questions asked here and here

The questions asked in the other posts have to do with mocking methods that only take one argument.

However, my method takes multiple arguments and I need something like this:

$mock->expects($this->exactly(3))
->method('MyMockedMethod')
    ->with(
        $this->logicalOr(
            $this->equalTo($arg1, $arg2, arg3....argNb),
            $this->equalTo($arg1b, $arg2b, arg3b....argNb),
            $this->equalTo($arg1c, $arg2c, arg3c....argNc)
        )
    );

This code doesn't work because equalTo() validates only one argument. Giving it more than one argument throws an exception:

Argument #2 of PHPUnit_Framework_Constraint_IsEqual::__construct() must be a numeric

Is there a way to do a logicalOr mocking for a method with more than one argument?

starball
  • 20,030
  • 7
  • 43
  • 238
Thomas
  • 962
  • 1
  • 6
  • 12
  • You have not formulated a question (with real words). Also you have not explained why the code you added specifically does not work for you. That might sound redundant, but that information will help to make your question more clear and easier to provide an answer. We can not look into your brain. – hakre Jun 09 '12 at 15:41
  • @Thomas: I came here to ask this *exact* question, so I updated and upvoted yours, I hope you don't mind. (And now we play the waiting game...) – dr Hannibal Lecter Jun 09 '12 at 20:45

4 Answers4

99

In my case the answer turned out to be quite simple:

$this->expects($this->at(0))
    ->method('write')
    ->with(/* first set of params */);

$this->expects($this->at(1))
    ->method('write')
    ->with(/* second set of params */);

The key is to use $this->at(n), with n being the Nth call of the method. I couldn't do anything with any of the logicalOr() variants I tried.

dr Hannibal Lecter
  • 6,665
  • 4
  • 33
  • 42
  • I think the key here is that `with` takes a series of constraints (implying an `equalTo` if just a value is given). You *should* be able to use `logicalOr`, but only in the context of each argument separately (which might not be very useful). – John Flatness Jun 09 '12 at 21:20
  • @JohnFlatness: You are right, I *should*! :) I am surprised by the fact that something so basic is limited to one argument. – dr Hannibal Lecter Jun 09 '12 at 22:04
  • 2
    @drHannibalLecter This works, but the problem here is that it hard codes your test to care about the internal implementation of the test case. – Thomas Jun 11 '12 at 13:09
  • @drHannibalLecter I will probably mark this as the answer, but I would like to wait and see of there is anyone has a solution with the logicalor implementation. – Thomas Jun 11 '12 at 13:14
  • @Thomas: I agree that it's bad, but unless PHPUnit guys do something about it, I think we're stuck with it.. :-\ – dr Hannibal Lecter Jun 11 '12 at 16:36
  • 1
    @drHannibalLecter It seems the index starts at "1" and not "0". – Geoffrey Brier Jan 09 '14 at 19:08
  • @GeoffreyBrier: my usage as tested right now shows that the index begins at 0. – ashnazg Feb 16 '14 at 17:23
  • 3
    Just a note here: The index for `$this->at()` starts with zero, but increases by _every_ method-call on the mocked object. @GeoffreyBrier, that may've been the reason why it, for you, was the index 1 .... Read here for more info: http://phpunit.de/manual/current/en/test-doubles.html#test-doubles.mock-objects.tables.matchers – SimonSimCity Feb 20 '14 at 16:57
  • Thank you @SimonSimCity!! Didn't think it'd be EVERY method call, this is so unstable :( – masterchief Nov 20 '15 at 01:05
  • 4
    quick note at matcher is deprecated and will be removed in phpunit 10, cody's answer should be used instead, including withConsecutive – Rishiraj Purohit Mar 01 '21 at 14:37
  • @RishirajPurohit We cannot use cody's answer as InvocationMocker::withConsecutive() is deprecated in PHPUnit 9.6 and removed from PHPUnit 10. – chithra Apr 28 '23 at 11:16
71

For others who are looking to both match input parameters and provide return values for multiple calls.. this works for me:

    $mock->method('myMockedMethod')
         ->withConsecutive([$argA1, $argA2], [$argB1, $argB2], [$argC1, $argC2])
         ->willReturnOnConsecutiveCalls($retValue1, $retValue2, $retValue3);
Cody A. Ray
  • 5,869
  • 1
  • 37
  • 31
34

Stubbing a method call to return the value from a map

$map = array(
    array('arg1_1', 'arg2_1', 'arg3_1', 'return_1'),
    array('arg1_2', 'arg2_2', 'arg3_2', 'return_2'),
    array('arg1_3', 'arg2_3', 'arg3_3', 'return_3'),
);
$mock->expects($this->exactly(3))
    ->method('MyMockedMethod')
    ->will($this->returnValueMap($map));

Or you can use

$mock->expects($this->exactly(3))
    ->method('MyMockedMethod')
    ->will($this->onConsecutiveCalls('return_1', 'return_2', 'return_3'));

if you don't need to specify input arguments

scriptin
  • 3,060
  • 1
  • 24
  • 33
14

In case someone finds this without looking at the correspondent section in the phpunit documentation, you can use the withConsecutive method

$mock->expects($this->exactly(3))
     ->method('MyMockedMethod')
     ->withConsecutive(
         [$arg1, $arg2, $arg3....$argNb],
         [arg1b, $arg2b, $arg3b....$argNb],
         [$arg1c, $arg2c, $arg3c....$argNc]
         ...
     );

The only downside of this being that the code MUST call the MyMockedMethod in the order of arguments supplied. I have not yet found a way around this.

Pedro A.
  • 141
  • 1
  • 2
  • 2
    I believe this is what the "map" functions are for e.g. willReturnMap or returnValueMap - like a lookup from params to returns. – scipilot Apr 24 '17 at 05:02