157

Is there any way to define different mock-expects for different input arguments? For example, I have database layer class called DB. This class has method called Query(string $query), that method takes an SQL query string on input. Can I create mock for this class (DB) and set different return values for different Query method calls that depends on input query string?

gturri
  • 13,807
  • 9
  • 40
  • 57
Aleksei Kornushkin
  • 2,219
  • 5
  • 21
  • 18
  • In addition to the answer below, you can also use the method in this answer: http://stackoverflow.com/questions/5484602/mock-in-phpunit-multiple-configuration-of-the-same-method-with-different-argum – Schleis Apr 08 '13 at 15:32
  • I like this answer http://stackoverflow.com/a/10964562/614709 – yitznewton Jan 24 '14 at 13:38

6 Answers6

277

It's not ideal to use at() if you can avoid it because as their docs claim

The $index parameter for the at() matcher refers to the index, starting at zero, in all method invocations for a given mock object. Exercise caution when using this matcher as it can lead to brittle tests which are too closely tied to specific implementation details.

Since 4.1 you can use withConsecutive eg.

$mock->expects($this->exactly(2))
     ->method('set')
     ->withConsecutive(
         [$this->equalTo('foo'), $this->greaterThan(0)],
         [$this->equalTo('bar'), $this->greaterThan(0)]
       );

If you want to make it return on consecutive calls:

  $mock->method('set')
         ->withConsecutive([$argA1, $argA2], [$argB1], [$argC1, $argC2])
         ->willReturnOnConsecutiveCalls($retValueA, $retValueB, $retValueC);

PHPUnit 10 removed withConsecutive. You can get similar functionality with:

$mock->expects($this->exactly(2))
    ->method('set')
    ->willReturnCallback(fn (string $property, int $value) => match (true) {
        $property === 'foo' && $value > 0,
        $property === 'bar' && $value > 0 => $mock->$property = $value,
        default => throw new LogicException()
    });

Obviously way uglier and not quite the same, but that's the state of things. You can read more about alternatives here: https://github.com/sebastianbergmann/phpunit/issues/4026 and here: https://github.com/sebastianbergmann/phpunit/issues/4026#issuecomment-825453794

hirowatari
  • 3,195
  • 2
  • 14
  • 15
152

The PHPUnit Mocking library (by default) determines whether an expectation matches based solely on the matcher passed to expects parameter and the constraint passed to method. Because of this, two expect calls that only differ in the arguments passed to with will fail because both will match but only one will verify as having the expected behavior. See the reproduction case after the actual working example.


For you problem you need to use ->at() or ->will($this->returnCallback( as outlined in another question on the subject.

Example:

<?php

class DB {
    public function Query($sSql) {
        return "";
    }
}

class fooTest extends PHPUnit_Framework_TestCase {


    public function testMock() {

        $mock = $this->getMock('DB', array('Query'));

        $mock
            ->expects($this->exactly(2))
            ->method('Query')
            ->with($this->logicalOr(
                 $this->equalTo('select * from roles'),
                 $this->equalTo('select * from users')
             ))
            ->will($this->returnCallback(array($this, 'myCallback')));

        var_dump($mock->Query("select * from users"));
        var_dump($mock->Query("select * from roles"));
    }

    public function myCallback($foo) {
        return "Called back: $foo";
    }
}

Reproduces:

phpunit foo.php
PHPUnit 3.5.13 by Sebastian Bergmann.

string(32) "Called back: select * from users"
string(32) "Called back: select * from roles"
.

Time: 0 seconds, Memory: 4.25Mb

OK (1 test, 1 assertion)


Reproduce why two ->with() calls don't work:

<?php

class DB {
    public function Query($sSql) {
        return "";
    }
}

class fooTest extends PHPUnit_Framework_TestCase {


    public function testMock() {

        $mock = $this->getMock('DB', array('Query'));
        $mock
            ->expects($this->once())
            ->method('Query')
            ->with($this->equalTo('select * from users'))
            ->will($this->returnValue(array('fred', 'wilma', 'barney')));

        $mock
            ->expects($this->once())
            ->method('Query')
            ->with($this->equalTo('select * from roles'))
            ->will($this->returnValue(array('admin', 'user')));

        var_dump($mock->Query("select * from users"));
        var_dump($mock->Query("select * from roles"));
    }

}

Results in

 phpunit foo.php
PHPUnit 3.5.13 by Sebastian Bergmann.

F

Time: 0 seconds, Memory: 4.25Mb

There was 1 failure:

1) fooTest::testMock
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-select * from roles
+select * from users

/home/.../foo.php:27

FAILURES!
Tests: 1, Assertions: 0, Failures: 1
danronmoon
  • 3,814
  • 5
  • 34
  • 56
edorian
  • 38,542
  • 15
  • 125
  • 143
  • 8
    thanks for your help! Your answer completely solved my problem. P.S. Sometimes TDD development seems terrifying to me when I have to use such large solutions for simple architecture :) – Aleksei Kornushkin May 13 '11 at 12:38
  • 1
    This is a great answer, really helped me understand PHPUnit mocks. Thanks!! – Steve Bauman May 22 '16 at 04:18
  • You can also use `$this->anything()` as one of the parameters to `->logicalOr()` to allow you to provide a default value for other arguments than the one you're interested in. – MatsLindh Jan 27 '17 at 13:31
  • 4
    Am wondering nobody mentions, that with "->logicalOr()" you won't guarantee that (in this case) both of the arguments have been called. So this doesn't really solve the problem. – ssibal Jul 24 '17 at 13:04
28

From what I've found, the best way to solve this problem is by using PHPUnit's value-map functionality.

Example from PHPUnit's documentation:

class SomeClass {
    public function doSomething() {}   
}

class StubTest extends \PHPUnit_Framework_TestCase {
    public function testReturnValueMapStub() {

        $mock = $this->getMock('SomeClass');

        // Create a map of arguments to return values.
        $map = array(
          array('a', 'b', 'd'),
          array('e', 'f', 'h')
        );  

        // Configure the mock.
        $mock->expects($this->any())
             ->method('doSomething')
             ->will($this->returnValueMap($map));

        // $mock->doSomething() returns different values depending on
        // the provided arguments.
        $this->assertEquals('d', $stub->doSomething('a', 'b'));
        $this->assertEquals('h', $stub->doSomething('e', 'f'));
    }
}

This test passes. As you can see:

  • when the function is called with parameters "a" and "b", "d" is returned
  • when the function is called with parameters "e" and "f", "h" is returned

From what I can tell, this feature was introduced in PHPUnit 3.6, so it's "old" enough that it can be safely used on pretty much any development or staging environments and with any continuous integration tool.

Radu Murzea
  • 10,724
  • 10
  • 47
  • 69
  • returnValueMap() use === to compare parameters: in case of Object that comparison failed as php documentation says https://www.php.net/manual/en/language.oop5.object-comparison.php – Luca Camillo Mar 02 '23 at 13:59
6

It seems Mockery (https://github.com/padraic/mockery) supports this. In my case I want to check that 2 indices are created on a database:

Mockery, works:

use Mockery as m;

//...

$coll = m::mock(MongoCollection::class);
$db = m::mock(MongoDB::class);

$db->shouldReceive('selectCollection')->withAnyArgs()->times(1)->andReturn($coll);
$coll->shouldReceive('createIndex')->times(1)->with(['foo' => true]);
$coll->shouldReceive('createIndex')->times(1)->with(['bar' => true], ['unique' => true]);

new MyCollection($db);

PHPUnit, this fails:

$coll = $this->getMockBuilder(MongoCollection::class)->disableOriginalConstructor()->getMock();
$db  = $this->getMockBuilder(MongoDB::class)->disableOriginalConstructor()->getMock();

$db->expects($this->once())->method('selectCollection')->with($this->anything())->willReturn($coll);
$coll->expects($this->atLeastOnce())->method('createIndex')->with(['foo' => true]);
$coll->expects($this->atLeastOnce())->method('createIndex')->with(['bar' => true], ['unique' => true]);

new MyCollection($db);

Mockery also has a nicer syntax IMHO. It appears to be a tad slower than PHPUnits built-in mocking capability, but YMMV.

joerx
  • 2,028
  • 1
  • 16
  • 18
1

We are trying to upgrade out tests with Phpunit10 on PHP8.1 as annual upgrade of our images/libraries.
On Phpunit10 at() & withConsecutive() are deprecated.

@Radu Murzea's solution works in most cases: not ours!
I need to mock MongoDB calls: parameters sometime are MongoDB\ObjectId; returnValueMap() use === to compare parameters reiceved: in case of Object that comparison failed as php documentation says php.net/manual/en/language.oop5.object-comparison.php

my solution to mock MongoDB FindOne is the following:

    $map = [
        [
            ['_id' => new ObjectId("5825cfc1316f54c6128b4572"),],
            [],
            ['_id' => new ObjectId("5825cfc1316f54c6128b4572"), 'username' => 'test']
        ],
        [
            ['agencyIds' => new ObjectId("5825cfc1316f54c6128b4572"),],
            ['_id'],
            false
        ],
        [
            ['agencyIds' => new ObjectId("5825cfc1316f54c6128b4572"),],
            ['_id'],
            false
        ],
    ];

    $mongoDBUsersCollectionMock = $this->createMock(MongoDBCollection::class);
    $mongoDBUsersCollectionMock
        ->method('findOne')
        ->with($this->anything())
        ->will($this->returnCallback(
            function($filter, $options) use (&$map){
                list($mockedFilter, $mockedOptions, $mockedReturn) = array_shift($map);
                // if contains object remember don't use === because mean the exactly the same object
                // ref: https://www.php.net/manual/en/language.oop5.object-comparison.php
                if ($filter == $mockedFilter && $options == $mockedOptions){
                    return $mockedReturn;
                }
            }
        ));
Luca Camillo
  • 796
  • 9
  • 9
0

Intro

Okay I see there is one solution provided for Mockery, so as I don't like Mockery, I am going to give you a Prophecy alternative but I would suggest you first to read about the difference between Mockery and Prophecy first.

Long story short: "Prophecy uses approach called message binding - it means that behaviour of the method does not change over time, but rather is changed by the other method."

Real world problematic code to cover

class Processor
{
    /**
     * @var MutatorResolver
     */
    private $mutatorResolver;

    /**
     * @var ChunksStorage
     */
    private $chunksStorage;

    /**
     * @param MutatorResolver $mutatorResolver
     * @param ChunksStorage   $chunksStorage
     */
    public function __construct(MutatorResolver $mutatorResolver, ChunksStorage $chunksStorage)
    {
        $this->mutatorResolver = $mutatorResolver;
        $this->chunksStorage   = $chunksStorage;
    }

    /**
     * @param Chunk $chunk
     *
     * @return bool
     */
    public function process(Chunk $chunk): bool
    {
        $mutator = $this->mutatorResolver->resolve($chunk);

        try {
            $chunk->processingInProgress();
            $this->chunksStorage->updateChunk($chunk);

            $mutator->mutate($chunk);

            $chunk->processingAccepted();
            $this->chunksStorage->updateChunk($chunk);
        }
        catch (UnableToMutateChunkException $exception) {
            $chunk->processingRejected();
            $this->chunksStorage->updateChunk($chunk);

            // Log the exception, maybe together with Chunk insert them into PostProcessing Queue
        }

        return false;
    }
}

PhpUnit Prophecy solution

class ProcessorTest extends ChunkTestCase
{
    /**
     * @var Processor
     */
    private $processor;

    /**
     * @var MutatorResolver|ObjectProphecy
     */
    private $mutatorResolverProphecy;

    /**
     * @var ChunksStorage|ObjectProphecy
     */
    private $chunkStorage;

    public function setUp()
    {
        $this->mutatorResolverProphecy = $this->prophesize(MutatorResolver::class);
        $this->chunkStorage            = $this->prophesize(ChunksStorage::class);

        $this->processor = new Processor(
            $this->mutatorResolverProphecy->reveal(),
            $this->chunkStorage->reveal()
        );
    }

    public function testProcessShouldPersistChunkInCorrectStatusBeforeAndAfterTheMutateOperation()
    {
        $self = $this;

        // Chunk is always passed with ACK_BY_QUEUE status to process()
        $chunk = $this->createChunk();
        $chunk->ackByQueue();

        $campaignMutatorMock = $self->prophesize(CampaignMutator::class);
        $campaignMutatorMock
            ->mutate($chunk)
            ->shouldBeCalled();

        $this->mutatorResolverProphecy
            ->resolve($chunk)
            ->shouldBeCalled()
            ->willReturn($campaignMutatorMock->reveal());

        $this->chunkStorage
            ->updateChunk($chunk)
            ->shouldBeCalled()
            ->will(
                function($args) use ($self) {
                    $chunk = $args[0];
                    $self->assertTrue($chunk->status() === Chunk::STATUS_PROCESSING_IN_PROGRESS);

                    $self->chunkStorage
                        ->updateChunk($chunk)
                        ->shouldBeCalled()
                        ->will(
                            function($args) use ($self) {
                                $chunk = $args[0];
                                $self->assertTrue($chunk->status() === Chunk::STATUS_PROCESSING_UPLOAD_ACCEPTED);

                                return true;
                            }
                        );

                    return true;
                }
            );

        $this->processor->process($chunk);
    }
}

Summary

Once again, Prophecy is more awesome! My trick is to leverage the messaging binding nature of Prophecy and even though it sadly looks like a typical, callback javascript hell code, starting with $self = $this; as you very rarely have to write unit tests like this I think it's a nice solution and it's definitely easy to follow, debug, as it actually describes the program execution.

BTW: There is a second alternative but requires changing the code we are testing. We could wrap the troublemakers and move them to a separate class:

$chunk->processingInProgress();
$this->chunksStorage->updateChunk($chunk);

could be wrapped as:

$processorChunkStorage->persistChunkToInProgress($chunk);

and that's it but as I didn't want to create another class for it, I prefer the first one.

Nunser
  • 4,512
  • 8
  • 25
  • 37
Lukas Lukac
  • 7,766
  • 10
  • 65
  • 75