I'm trying to mock out the actual database operations in my Laravel application for the purposes of effective unit testing. But I am having difficulty in getting the mocked method to be called, rather than the real method.
I have about 200 Models in app/Models
which were generated using artisan make:model
. I'm in the process of creating query builder classes which extend Illuminate\Database\Eloquent\Builder
for each of the models. Likewise, there will be a PHPUnit test class for each of these query builders.
I've spent the last 2 days with the Laravel docs, the Mockery docs, Stack Overflow, Google and lots of debugging trying to get my unit test to work correctly, to no avail. What's the trick to making this work with Laravel's intricate internals?
Example Model, stripped down:
<?php
namespace App\Models;
use App\Domain\Example\QueryBuilders\ExampleQueryBuilder;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $foo
* @property string $bar
* @property string $baz
*/
class Example extends Model
{
public $timestamps = false;
protected $table = 'my_table';
protected $fillable = ['foo', 'bar', 'baz',];
protected $dateFormat = 'U';
protected $connection = 'mysql';
public function newEloquentBuilder($query): ExampleQueryBuilder {
return new ExampleQueryBuilder($query);
}
}
Example query builder, stripped down:
<?php
namespace App\Domain\Example\QueryBuilders;
use App\Models\Example;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
final class ExampleQueryBuilder extends Builder
{
protected $model;
protected $onDelete;
protected $query;
public function findActive(string $foo): ?Model {
$conditions = ['foo' => "$foo", 'bar' => 1,];
$example = Example::firstWhere($conditions);
if ($example instanceof Example) {return $example;}
return null;
}
Example unit test:
<?php
namespace Tests\Domain\Example\QueryBuilders;
use App\Models\Example;
use Tests\TestCase;
class ExampleQueryBuilderTest extends TestCase
{
/** @test */
public function it_handles_missing_example_correctly(): void {
// I've tried endless variations on this, mocking the Model "Example",
// mocking "Model", even "Process" at one crazy point. Debugging proves
// without a doubt that the `Builder::firstWhere` method IS being called
// instead of my mocked method.
$mock = $this->partialMock(Builder::class, function (MockInterface $mock) {
$mock->shouldReceive('firstWhere')->once();
});
$this->app->instance(Builder::class, $mock);
$example = Example::findActive('');
self::assertNull($example);
}
}
When this test is run, I get this output:
Method firstWhere() from Mockery_0_Illuminate_Database_Eloquent_Builder should be called exactly 1 times but called 0 times.
So clearly the method I need to mock out is not getting mocked somehow. What am I overlooking or missing?
My environment: Laravel 8, Mockery 1.4, PHPUnit 9, PHP 7.4/8.0, macOS/Linux