10

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 .

Asem Khatib
  • 484
  • 7
  • 14

3 Answers3

11

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);
Shady Keshk
  • 540
  • 6
  • 16
5

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.

BVengerov
  • 2,947
  • 1
  • 18
  • 32
  • Thanks but when I tried this : http://pastie.org/private/cpxfphhjbl2y4ih7iudjra it didn't work , I got this error : – Asem Khatib Oct 18 '16 at 10:39
  • PHP Fatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::getFinder() in /var/www/public/mypoll/tests/unit/classes/PaginationTest.php on line 56 – Asem Khatib Oct 18 '16 at 10:40
  • @ebncana That's strange... Could you debug inside the `Pagination`'s constructor to check what object you are getting there? – BVengerov Oct 18 '16 at 11:01
  • I could not run the debug from the CLI but this is Pagination class : http://pastie.org/private/lcd33hz5et32iu0amzlsqw – Asem Khatib Oct 18 '16 at 11:30
  • @ebncana The fact that it doesn't work bugs me... What happens if you try [the following code](http://pastie.org/10945375)? – BVengerov Oct 18 '16 at 12:58
3

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.

Pieter van den Ham
  • 4,381
  • 3
  • 26
  • 41
  • Thanks @Pete but it's gonna be really painful to inject finder and toolbox of RedBeanPHP every time to each class to be able to test it properly , checkout this last commit : https://github.com/AsemKhatib/MyPoll-OOP-PHP-Poll-system/commit/709bddfd2fcd8fcbbc1ca8cceb6d11585131fd68 So I need to inject both of them inside each instant in the factory : https://github.com/AsemKhatib/MyPoll-OOP-PHP-Poll-system/blob/testing/classes/factory.class.php Instead of just inject the whole db class. I'm really confused here . – Asem Khatib Oct 18 '16 at 10:47
  • Your `Factory` is an implementation of the "Service Locator" pattern (e.g. all classes receive the locator and retrieve the services they actually need from them). The easiest solution might be to mock the Locator (Factory). However, this requires that you know exactly what dependencies the class under test requires (which is why some people consider this an [anti-pattern](http://stackoverflow.com/questions/22795459/is-servicelocator-an-anti-pattern)). Have you considered using a dependency injection framework such as PHP-DI? – Pieter van den Ham Oct 18 '16 at 11:33
  • Note that such a framework is not strictly necessary, but it resolves the "really painful" aspect of pure dependency injection without framework. – Pieter van den Ham Oct 18 '16 at 11:37
  • thanks @Pete , I will try to implement the DI after removing the factory and will let you know what happened , thanks a lot . – Asem Khatib Oct 18 '16 at 12:10
  • @Pete I don't get it. But now problem with testing chained mathods is moved to another class. Now he need to unit test these chains in Finder class. Am I wrong? – user6827096 Dec 14 '16 at 11:36