20

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.

myol
  • 8,857
  • 19
  • 82
  • 143

3 Answers3

29

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');
schellingerht
  • 5,726
  • 2
  • 28
  • 56
Fabio Antunes
  • 22,251
  • 15
  • 81
  • 96
19

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.

Torkil Johnsen
  • 2,375
  • 1
  • 18
  • 18
  • Thanks for this response, it's been very helpful. I feel like I'm still missing something, as my mock sitll implements something like Mockery_0_My_Class_Namespace... I know this is an old post, but maybe you could help me here, instead of me asking the same quesiton... `$mockCurl = m::mock(HttpRequest::class); $mockCurl->shouldReceive(...` _RealTag::__construct() must be an instance of westonwatson\HttpRequest or null, instance of Mockery_0_westonwatson_realtag_HttpRequest given_ – Weston Watson May 24 '19 at 15:31
  • Using `PersonModel::class` exactly solved my problem that led me to this question. Thank you! – craastad Mar 03 '20 at 08:38
0

I think it's fine to fluently chain on the shouldReceive() method when creating mocks. If you chain the getMock() function to the end of the statement where you create your mock, you will have the instance returned.

So:

$mock = Mockery::mock('Myname\Myapp\Models\PersonModel')
    ->shouldReceive('find')
    ->with('var')
    ->getMock();
damask
  • 529
  • 4
  • 17