Reputation: 514
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
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
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
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