Asem Khatib
Asem Khatib

Reputation: 514

How to mock chain of methods with PHPUnit test

I'm trying to mock a chain (nested) of methods to return the desired value , this is the code :

public function __construct($db)
{
$this->db = $db;
}

public function getResults()
{
return $this->db->getFinder()->find($this->DBTable);
}

I tried this mock but it does not work :

$dbMock = $this->createMock(DB::class);
        $dbMock = $dbMock
            ->expects(self::any())
            ->method('getFinder')
            ->method('find')
            ->with('questions')
            ->will($this->returnValue('7'));

Any solutions how to solve such a problem ?

Thank you .

Upvotes: 11

Views: 10704

Answers (3)

Shady Keshk
Shady Keshk

Reputation: 570

it's simpler now with Mocking Demeter Chains And Fluent Interfaces

simply

$dbMock = $dbMock
        ->expects(self::any())
        ->method('getFinder->find')
        ->with('questions')
        ->will($this->returnValue('7'));

another example from mockery docs

$object->foo()->bar()->zebra()->alpha()->selfDestruct();

and you want to make selfDestruct to return 10

$mock = \Mockery::mock('CaptainsConsole');
$mock->shouldReceive('foo->bar->zebra->alpha->selfDestruct')->andReturn(10);

Upvotes: 13

Pieter van den Ham
Pieter van den Ham

Reputation: 4514

While @BVengerov his answer will most definitely work, I suggest a design change instead. I believe that chaining mocks is not the way to go, it hurts readability and, more importantly, simplicity of your tests.

I propose that you make the Finder class a member of your class. As such, you now only have to mock out the Finder.

class MyClass {

    private $finder;

    public function __construct(Finder $finder) {
        $this->finder = $finder;
    }

    public function getResults() {
        return $this->finder->find($this->DBTable);
    }
}

This change makes unittesting this function (and class!) simple.

"But I need the $db variable in other places of the class!" Well, first and foremost, that probably indicates that a class in your current class is dying to be extracted. Keep classes small and simple.

However, as a quick-and-dirty solution, consider adding a setFinder() setter, just to be used by tests.

Upvotes: 3

BVengerov
BVengerov

Reputation: 3007

A chain consists of objects being called one after another. Therefore, You need to implement a chain of mocks. Just mock the methods in such a way that they return the mocked objects.

Something like this should work:

$finderMock = $this->createMock(Finder::class);
$finderMock = $finderMock
    ->expects(self::any)
    ->method('find')
    ->with('questions')
    ->will($this->returnValue('7'));

$dbMock = $this->createMock(DB::class);
$dbMock = $dbMock
    ->expects(self::any())
    ->method('getFinder')
    ->will($this->returnValue($finderMock));

Read more about mock chaining in this cool blog post.

I don't really see the point in testing chains, though. IMO it's better to limit tests to testing 1 module (function) or 2 modules (interaction) at a time.

Upvotes: 5

Related Questions