Summer Sale! All accounts are 50% off this week.

xdimension's avatar

Is it possible to mock Model::find()?

Hi,

I have this code and I want to test that the model's attributes are assigned properly:

class SubTask extends Model
{
    public function add($data)
    {
        $this->fill($data);

        $task = Task::find($data['parent_task_id']);

        $this->project_id = $task->project->id;

        $this->save();
    }
}

Is it possible to mock Task::find so it can provide mocked $task object where I can set $task->project->id?

Here is the test that I'm thinking of:

    public function add_subtask()
    {
        $data = [
            'name' => 'Subtask One',
            'parent_task_id' => 1
        ];

        // need to mock Task::find here

        $subTask = m::mock('SubTask')->makePartial();
        $subTask->shouldReceive('save')->once();

        $subTask->add($data);

        $this->assertEquals($subTask->name, $data['name']);
        $this->assertEquals($subTask->project_id, $task->project->id);      
    }
0 likes
4 replies
bobbybouwmann's avatar

You can probably do something like this

$modelMock = Mockery::mock('App\Model');

// Other stuff here

$this->app->instance('App\Model', $modelMock);
1 like
Corez64's avatar
Corez64
Best Answer
Level 37

What bobby suggested would work for normal facades but models don't work that way. It is actually very difficult to unit test models and it is best to try and treat them like value objects as much as possible.

I think in terms of just getting this to work for you would be to use Laravel's IoC container to get the Task model so you would have $task = app(Task::class)->find($data['parent_task_id']);. This way what bobby has suggest would work.

I would suggest perhaps creating a command that way you can have dependency injection working.

class CreateSubTaskJob extends Job implements SelfHandling
{
    public $data;

    public function __construct($name, $other, $attributes, $parent_task_id)
    {
        $data = compact('name', 'other', 'attributes', 'parent_task_id');
    }

    pubilc function handle(Task $taskRepo, SubTask $subTaskRepo)
    {
        $this->data['project_id'] = $taskRepo->find($this->data['parent_task_id'])->project->id;
        return $subTaskRepo->create($this->data);
    }
}

Then in your controller or where ever you can dispatch the command and it will handle everything for you:

class SubTaskController extends Controller
{
    // ...
    public function store(Request $request)
    {
        $subTask = $this->dispatchFrom(CreateSubTaskJob::class, $request);

        // Do stuff

        return redirect()->back()->with('message', 'Created successfully!');
    }

    // ...
}

Or in your testing:

/** @test **/
function it_creates_subtask() 
{
    $mockTask = Mockery::mock(Task::class);
    $mockTask->shouldReceive('find')->with(15)->andReturn((object) ['project' => (object) ['id' => 3]);
    $mockSubTask = Mockery::mock(SubTask::class);
    $mockSubTask->shouldReceive('create');
    (new CreateSubTaskJob('Name', 'Other', 'Attributes', 15))->handle($mockTask, $mockSubTask);
}
5 likes
xdimension's avatar

@bobbybouwmann Thanks for your reply. I have just tried this:

        $project = m::mock('StdClass');
        $project->id = 1;

        $task = m::mock('App\Task');
        $task->project = $project;

    $this->app->instance('App\Task', $task); 

got no luck, it raised an error "Method Mockery_1_App_Task::setAttribute() does not exist on this mock object", it must be this line $task->project = $project caused the trouble and I don't think I want to set an attribute here.

I also tried:

        $project = m::mock('StdClass');
        $project->id = 1;

        $task = m::mock('StdClass');
        $task->project = $project;
    $task->shouldReceive('find')->once()->andReturn($task);

    $this->app->instance('App\Task', $task); 

It seemed to work, but it failed in asserting $subTask->project_id equals to 1.

Anyway, I'm curious, is the trouble becauseTask::find is actually a static call? If it is so, I'm wondering what is the workaround for this case?

@Corez64 Okay, thanks, I posted this before seeing there's a new reply from you. I will see your solution. Thanks!

kmuenkel's avatar

Think I have a partial solution for you. It involves swapping out the Connection class for a dummy-one.

<?php

namespace App\Tests\Mocks;

use PDO;
use Closure;
use PDOException;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\Query\Processors\Processor;

class DummyConnection extends MySqlConnection
{
    const DRIVER = 'mysql';

    protected function runQueryCallback($query, $bindings, Closure $callback)
    {
        return [];
    }

    protected function getDefaultPostProcessor(): Processor
    {
        return new class extends Processor
        {
            public function processInsertGetId(Builder $query, $sql, $values, $sequence = null): bool|int|string
            {
                try {
                    return parent::processInsertGetId($query, $sql, $values, $sequence);
                } catch (PDOException $exception) {
                    return 1;
                }
            }
        };
    }

    protected function createTransaction()
    {
        //
    }

    public function commit()
    {
        //
    }

    public static function mock()
    {
        static::resolverFor(static::DRIVER, fn (
            Closure|PDO $pdo,
            string $database,
            string $prefix,
            array $config
        ) => app(static::class, compact('pdo', 'database', 'prefix', 'config')));

        app('db')->purge();
    }
}

You could easily modify this to make runQueryCallback() return some fake content after running some checks against the query and bindings parameters. Then all you need to do is call DummyConnection::mock(); from anywhere in your test class.

Please or to participate in this conversation.