12
votes

I am using laravel 5 with php unit to create a laravel package. I have a Repository..

namespace Myname\Myapp\Repositories;

use Myname\Myapp\Models\PersonModel;

class PersonRepository
{
    protected $personModel;

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

    public function testFunction($var)
    {
        return $this->personModel->find($var);
    }
}

..which implements a Model.

namespace Myname\Myapp\Models;

use Illuminate\Database\Eloquent\Model;

class PersonModel extends Model
{
    protected $table = 'person';
}

Laravels IoC automatically injects PersonModel into the constructor of PersonRepository.

I am writing a unit test where I want to mock the PersonModel model using mockery so I am not hitting the database during testing.

namespace Myname\Myapptests\unit;

use Mockery;

class PersonRepositoryTest extends \Myname\Myapptests\TestCase
{
     /**
     * @test
     */ 
     public function it_returns_the_test_find()
     {
         $mock = Mockery::mock('Myname\Myapp\Models\PersonModel')
            ->shouldReceive('find')
            ->with('var');

         $this->app->instance('Myname\Myapp\Models\PersonModel', $mock);
         $repo = $this->app->make('Myname\Myapp\Repositories\PersonRepository');
         $result = $repo->testFunction('var');

         $this->assert...
     }
}

When I run the test I get an error

1) Myname\Myapptests\unit\PersonRepositoryTest::it_returns_the_test_find ErrorException: Argument 1 passed to Myname\Myapp\Repositories\PersonRepository::__construct() must be an instance of Myname\Myapp\Models\PersonModel, instance of Mockery\CompositeExpectation given

From what I have read, mockery extends the class it is mocking so there should be no issue injecting the extended class in place of the type hinted parent (PersonModel)

Obviously I am missing something. Other examples explicitly inject the mocked object into the class they are then testing. Laravels IoC is (should be) doing this for me. Do I have to bind anything?

I have a feeling though that the mockery object isn't being created in the way I think (extending PersonModel) otherwise I assume I wouldn't see this error.

2
Could you try to import your Model? you are using PersonModel::class but I don't see a use 'Myname\Myapp\Models\PersonModel'Fabio Antunes
@FabioAntunes Updated the code and the error outputmyol

2 Answers

20
votes

Problem is when you create your mock:

$mock = Mockery::mock('Myname\Myapp\Models\PersonModel')
    ->shouldReceive('find')
    ->with('var');

So this:

$mock = Mockery::mock('Myname\Myapp\Models\PersonModel')
var_dump($mock);
die();

Will output something like this: Mockery_0_Myname_Myapp_Models_PersonModel

But this:

$mock = Mockery::mock('Myname\Myapp\Models\PersonModel')
    ->shouldReceive('find')
    ->with('var');
var_dump($mock);
die();

Will output this: Mockery\CompositeExpectation

So try doing something like this:

$mock = Mockery::mock('Myname\Myapp\Models\PersonModel');
$mock->shouldReceive('find')->with('var');

$this->app->instance('Myname\Myapp\Models\PersonModel', $mock);
$repo = $this->app->make('Myname\Myapp\Repositories\PersonRepository');
$result = $repo->testFunction('var');
12
votes

While Fabio gives a great answer, the issue here is really the test setup. Mockery's mock objects do comply to contracts and will pass instanceof tests and type hints in method arguments.

The problem with the original code is that the chain of methods being called end up returning an expectation rather than a mock. We should instead create a mock first, then add expectations to that mock.

To fix it, change this:

$mock = Mockery::mock('Myname\Myapp\Models\PersonModel')
    ->shouldReceive('find')
    ->with('var');

Into this:

$mock = Mockery::mock('Myname\Myapp\Models\PersonModel');
$mock->shouldReceive('find')->with('var');

The variable $mock will now implement PersonModel.

Bonus:

Instead of 'Myname\Myapp\Models\PersonModel', use PersonModel::class. This is a lot more IDE-friendly and will help you when refactoring code later on.