Home > front end >  Laravel8 - Mocking methods from factory relationship models
Laravel8 - Mocking methods from factory relationship models

Time:09-17

I am trying to test a specific part of my code, but some dependencies of a Model class make this test very difficult to perform. If those dependencies were on a class that is injected, I could easily mock the class. Is it possible to do something similar when it comes from a database relationship from a factory? I am converting the idea to the example in Laravel's documentation, to try to make it easier to exemplify :

use App\Models\Post;
use App\Models\User;

$user = User::factory()
            ->has(Post::factory()->count(3))
            ->create();

Let's say that I am testing a controller that will create comments for posts of an user. I'll use a factory to create the user and post and perform a test post call to the route /api/comments, this post has the fields post_id (int) and comment (text). However, the Comments controller will call a method Post::canAddComment(), that will perform a lot of verifications to validate if the comment can be created. All those validations are out of the scope of my test.

Is it possible to use a Mock of the Post model class, so I can make, for example:

$postMock->shouldReceive('canAddComment')->once()->andReturn(true);

So that I don't need to be creating a whole scenario to be able to do the test?

Possible solution:

Extend the Post class, for tests only:

class PostThatAlwaysAllowsComments extends Post
{
    public function canAddComment() {
        return true;
    }
}

And then in the tests:

use App\Models\Post;
use App\Models\User;

$user = User::factory()
            ->has(PostThatAlwaysAllowsComments::factory()->count(3))
            ->create();

CodePudding user response:

In Laravel, mocking models is not really a standard or neither convenient. Most of the times, you arrange the data and let's model do what models does. In your case you can utilise a wrapper / proxy aproach, to easiler be able to mock your calls. This will also act as the entry point for mocking, as you need a class in the container to be mocked.

class PostThatAlwaysAllowsComments extends Post
{
    public function canAddComment() {
        resolve(CommentService::class)->comment($this);
    }
}

There is used resolve as to fetch the service from the container, as the new keyword will not work.

Now using Laravels mock syntaxic sugar, you can mock the underlying service and avoid triggering the code you do not care about and asserting it is actually called.

namespace Tests\Unit;

use App\Services\CommentService;
use Mockery\MockInterface;
use Tests\TestCase;

class PostAddComentTest extends TestCase
{
    public function testDoWork()
    {
        $post = // create your model with the factory;

        $mock = $this->mock(CommentService::class, function (MockInterface $mock) {
            $mock->shouldReceive('comment')
                ->once()
                ->andReturn(true);
        });

        // call api or method
    }
}
  • Related