laravel 使用 find() 模拟 Eloquent 模型

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/17008637/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-14 07:55:25  来源:igfitidea点击:

Mocking Eloquent Models with find()

phpunitlaravellaravel-4

提问by neyl

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

我正在尝试用 Mockery 模拟 Eloquent 模型。模型正在通过控制器注入控制器

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

Now I am calling the find()function in controller

现在我正在调用find()控制器中的函数

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

And here is test for PostsController

这是 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:

运行 PhpUnit 给我错误:

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().

这显然是因为find()被声明为静态函数。现在,代码可以正常工作,没有错误,那么我如何成功模拟 Eloquent 模型而不会失败。由于我们依赖于依赖注入,我必须find()非静态地调用,否则我只能做Post::find().

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

我想出的一个解决方案是find()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?

我做错了什么还是你有更好的想法?

回答by JeffreyWay

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.

Mocking Eloquent 模型是一件非常棘手的事情。这在书中有介绍,但我特别指出这是一个权宜之计。最好使用存储库。

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.

但是,要回答您的问题,问题在于您没有在构造函数中执行模拟。这是让它工作的唯一方法。这并不理想,我不会推荐它。

回答by Felix

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

我认为这就是原因,Jeffrey 在他的 Laravel 测试解码(第 10 章)一书中介绍了 Repositories。

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

Mockery 在其自述文件中也有关于静态方法的部分,请参阅https://github.com/padraic/mockery#mocking-public-static-methods

回答by chickenchilli

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

有一种方法可以做到这一点。(Laravel 版本是 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 hiHyman the find method within a "global static" scope.

让我们不讨论这个测试可能有多大用处,但我相信你明白了它的要点,并看到你将不再需要数据库,因为我们在“全局静态”范围内劫持了 find 方法。

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();
    }

}