Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 .

like image 557
Asem Khatib Avatar asked Oct 18 '16 08:10

Asem Khatib


People also ask

Which method is used to create a mock with PHPUnit?

PHPUnit provides methods that are used to automatically create objects that will replace the original object in our test. createMock($type) and getMockBuilder($type) methods are used to create mock object. The createMock method immediately returns a mock object of the specified type.

How do I run a PHPUnit test?

How to Run Tests in PHPUnit. You can run all the tests in a directory using the PHPUnit binary installed in your vendor folder. You can also run a single test by providing the path to the test file. You use the --verbose flag to get more information on the test status.

What is stub in PHPUnit?

Stub. Stubs are used with query like methods - methods that return things, but it's not important if they're actually called.


3 Answers

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);
like image 136
Shady Keshk Avatar answered Sep 24 '22 13:09

Shady Keshk


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.

like image 41
BVengerov Avatar answered Sep 22 '22 13:09

BVengerov


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.

like image 35
Pieter van den Ham Avatar answered Sep 23 '22 13:09

Pieter van den Ham