0
votes

I'm using Laravel 7 and trying to partial mock an eloquent model..

My controller has eloquent model dependency injected:

use App\Models\Ticket;

class TicketsController extends Controller
{
    /** @var Ticket */
    private $tickets;

    public function __construct(Ticket $ticket) {
        $this->tickets = $ticket;
    }

    public function get() {
        $tickets = $this->tickets::whereNull('staff_id')
            ->where('status', '!=', $this->tickets::STATUS_CLOSED)
            ->orderBy('created_at', 'desc')
            ->get();
    }
}

Unit test:

use App\Models\Ticket;

    $this->partialMock(Ticket::class, function($mock) {
        $mock->shouldReceive('get')
            ->once()
            ->andReturn([]);
    });

    $response = $this->json('get', route('tickets'));

    $response->assertOk();

Fails with:

testing.ERROR: Received Mockery_0_App_Models_Ticket::__construct(), but no expectations were specified {"exception":"[object] (Mockery\Exception\BadMethodCallException(code: 0): Received Mockery_0_App_Models_Ticket::__construct(), but no expectations were specified at /opt/project/vendor/mockery/mockery/library/Mockery/Loader/EvalLoader.php(34) : eval()'d code:911)

Also tried to change test for this:

    $ticketsMock = Mockery::mock(Ticket::class);
    $ticketsMock
        ->makePartial()
        ->shouldReceive('get')
        ->once()
        ->andReturn([]);

    $this->instance(Ticket::class, $ticketsMock);

But getting exactly same error..

If I replace mock for non partial one and fake all method calls on the model it works fine.. but some eloquent queries will be pretty long and I'm trying to have it done via partial mock so I don't have to fake every single chained call like ->shouldReceive('something')->andReturnSelf()

1
Protip: Don't mock laravel models it is a waste of time, sincerely someone who really likes mocking stuffmrhn
If I don't get a decent answer on this I'll just use repository pattern to wrap around models and mock that.. @mrhnSeva Kalashnikov
I have an answer, i still think it is a waste of time. Had to setup local environment to test the answer :)mrhn
Did my answer help your problem?mrhn

1 Answers

0
votes

The problem with partial, is that whereNull returns a QueryBuilder and not a Ticket model.

What i think is a good solution is mocking demeter chains. You can mock fluent calls, by doing expression it in a string whereNull->where->orderBy->get. I got the following example to work locally.

$this->mock(Ticket::class, function($mock) {
    $mock->shouldReceive('whereNull->where->orderBy->get')
        ->once()
        ->andReturn([]);
});

You still have to express the path of the calls to mock, but i think it is very reasonable and straight forward since you do not have to think of parameters and returns.

I changed the controller method a little since you called method on objects statically. To ensure it works, my controller logic was this.

$tickets = $this->tickets->whereNull('staff_id')
    ->where('status', '!=', Ticket::STATUS_CLOSED)
    ->orderBy('created_at', 'desc')
    ->get();