10
votes

I am trying to Mock Eloquent Model with Mockery. Model is being injected in Controller via

__construct(Post $model){$this->model=$model}

Now I am calling the find() function in controller

$post = $this->model->find($id);

And here is test for PostsController

class PostsTest extends TestCase{

      protected $mock;

      public function setUp() {
        parent::setUp();
        $this->mock = Mockery::mock('Eloquent', 'Posts'); /*According to Jeffrey Way you have to add Eloquent in this function call to be able to use certain methods from model*/
        $this->app->instance('Posts', $this->mock);
      }

      public function tearDown() {

        Mockery::close();
      }

      public function testGetEdit()
      {
        $this->mock->shouldReceive('find')->with(1)->once()->andReturn(array('id'=>1));

        $this->call('GET', 'admin/posts/edit/1');

        $this->assertViewHas('post', array('id'=>1));
      }
    }

Running PhpUnit gives me error:

Fatal error: Using $this when not in object context in ...\www\l4\vendor\mockery\mockery\library\Mockery\Generator.php(130) : eval()'d code on line 73

This is obviously because find() is declared as static function. Now, the code works without errors, so how can I successfully mock Eloquent model without it failing. Since we are relying on dependency injection, I have to call find() non-statically, otherwise I could just do Post::find().

One solution that I came up with is to create a non-static find() replacement in BaseModel

public function nsFind($id, $columns = array('*'))
{
  return self::find($id, $columns);
}

But this is a big pain as the function has to have different name!

Am I doing something wrong or do you have any better ideas?

3
This question - with answer implementing Jeferry Way's answer below worksneyl

3 Answers

7
votes

Mocking Eloquent models is a very tricky thing. It's covered in the book, but I specifically note that it's a stop-gap. It's better to use repositories.

However, to answer your question, the issue is that you're not performing the mock within the constructor. That's the only way to get it to work. It's not ideal, and I wouldn't recommend it.

2
votes

I think that is the reason, Jeffrey introduces Repositories in his book Laravel Testing Decoded (Chapter 10).

Mockery has a section about static methods in its README too, See https://github.com/padraic/mockery#mocking-public-static-methods

1
votes

There is a way of doing this half decent. (Laravel Version is 5.8)

Imagine you have a base class:

<?php

namespace App\Model;

use Illuminate\Database\Eloquent\Model as EloquentModel;
use Mockery\Mock;

/**
 * Base class of all model classes, to implement Observers or whatever filters you will need
 */
class Model extends EloquentModel
{

    protected static $mocks = [];

    /**
     * @return Mock
     */
    public static function getMock()
    {
        if (isset(self::$mocks[static::class])) {
            return self::$mocks[static::class];
        }
        self::$mocks[static::class] = \Mockery::mock(static::class)->makePartial()->shouldAllowMockingProtectedMethods();
        return self::$mocks[static::class];
    }

    public static function removeMock(): void
    {
        if (isset(self::$mocks[static::class])) {
            unset(self::$mocks[static::class]);
        }
    }

    public static function deleteMocks() : void
    {
        self::$mocks = [];
    }

    public static function __callStatic($method, $parameters)
    {
        /**
         * call the mock's function
         */
        if (isset(self::$mocks[static::class])) {
            return self::$mocks[static::class]->$method(...$parameters);
        }
        return parent::__callStatic($method, $parameters);
    }


}

A model would be:

<?php
namespace App\Model;

class Accounts extends Model
{
    /**
     * @var  string
     */
    protected $table = 'accounts';
    /**
     * @var  string
     */
    protected $primaryKey = 'account_id';

    /**
     * attributes not writable from outside
     * @var  mixed
     */
    protected $guarded = ['account_id'];
}

And then a service class giving you an account from the database:

<?php


namespace App\Service;

use App\Model\Accounts;

class AccountService
{
    public function getAccountById($id): ?Accounts
    {       
       return Accounts::find($id);       
    }

}

Let's not go into how useful this test might be, but I am sure you get the gist of it and see that you won't need a database anymore because we hijack the find method within a "global static" scope.

And the test would look something like this then:

<?php
namespace Tests\Unit\Services;

use App\Model\Accounts;
use App\Service\AccountService;
use Tests\TestCase;

class AccountServiceTest extends TestCase
{
    public function testGetAccountById()
    {
        $staticGlobalAccountMock = Accounts::getMock();
        $staticGlobalAccountMock->shouldReceive('find')
                ->andReturn(new Accounts(
                        ['account_id' => 123, 
                         'account_fax' => '056772']));
        $service = new AccountService();
        $ret = $service->getAccountById(123);
        $this->assertEquals('056772',$ret->account_fax);
        //clean up all mocks or just this mock
        Accounts::deleteMocks();
    }

}