【问题标题】:Laravel Tests pass to model to ViewLaravel 测试传递给模型以查看
【发布时间】:2015-06-21 14:35:29
【问题描述】:

我正确地模拟了我的存储库,但在像 show() 这样的情况下,它要么返回 null,因此视图最终会因为 在空对象上调用属性而导致测试崩溃。

我猜我应该模拟返回的 eloquent 模型,但我发现 2 个问题:

  1. 如果我最终还是要模拟 eloquent 模型,那么实现存储库模式的意义何在
  2. 你如何正确地模拟它们?下面的代码给了我一个错误。

    $this->mockRepository->shouldReceive('find')
        ->once()
        ->with(1)
        ->andReturn(Mockery::mock('MyNamespace\MyModel)
    
            // The view may call $book->title, so I'm guessing I have to mock
            // that call and it's returned value, but this doesn't work as it says
            // 'Undefined property: Mockery\CompositeExpectation::$title'
            ->shouldReceive('getAttribute')
            ->andReturn('')
    );
    

编辑:

我正在尝试测试控制器的操作,如下所示:

$this->call('GET', 'books/1'); // will call Controller#show(1)

事情是,在控制器的最后,它返回一个视图:

$book = Repo::find(1);
return view('books.show', compact('book'));

所以,测试用例也运行 view 方法,如果没有 $book 被模拟,它是 null 并崩溃

【问题讨论】:

  • 你到底想测试什么?是调用了正确的方法还是将某个模型传递给了视图?

标签: unit-testing laravel laravel-5 mockery


【解决方案1】:

因此,您正在尝试对控制器进行单元测试,以确保使用预期的参数调用正确的方法。控制器方法从存储库中获取模型并将其传递给视图。所以我们必须确保

  • 在 repo 上调用 find()-方法
  • repo 返回一个模型
  • 返回的模型被传递给视图

但首先要做的是:

如果我最终还是要模拟 eloquent 模型,那么实现存储库模式有什么意义?

除了通过不同来源的(可测试的)一致的数据访问规则、(可测试的)集中式缓存策略等之外,它还有许多用途。在这种情况下,您没有测试存储库,实际上您甚至都不关心返回的内容,您只是对调用某些方法感兴趣。所以结合依赖注入的概念,你现在有了一个强大的工具:你可以用 mock 切换 repo 的实际实例。

假设您的控制器如下所示:

class BookController extends Controller {

    protected $repo;

    public function __construct(MyNamespace\BookRepository $repo)
    {
        $this->repo = $repo;
    }

    public function show()
    {
        $book = $this->repo->find(1);

        return View::make('books.show', compact('book'));
    }
}

所以现在,在您的测试中,您只需模拟 repo 并将其绑定到容器:

public function testShowBook()
{
    // no need to mock this, just make sure you pass something
    // to the view that is (or acts like) a book
    $book = new MyNamespace\Book;

    $bookRepoMock = Mockery::mock('MyNamespace\BookRepository');

    // make sure the repo is queried with 1
    // and you want it to return the book instanciated above
    $bookRepoMock->shouldReceive('find')
                 ->once()
                 ->with(1)
                 ->andReturn($book);

    // bind your mock to the container, so whenever an instance of
    // MyNamespace\BookRepository is needed (like in your controller),
    // the mock will be loaded.
    $this->app->instance('MyNamespace\BookRepository', $bookRepoMock);

    // now trigger the controller method
    $response = $this->call('GET', 'books/1');

    $this->assertEquals(200, $response->getStatusCode());

    // check if the controller passed what was returned from the repo
    // to the view
    $this->assertViewHas('book', $book);
}

//编辑回应评论:

现在,在 testShowBook() 的第一行中,您实例化了一本新书,我假设它是 Eloquent\Model 的子类。这不会使整个控制反转[...]的交易无效吗?因为如果你改变 ORM,你仍然需要改变 Book,这样它就不会是 Model 的类

嗯……是的,不是的。是的,我已经在测试中直接实例化了模型类,但是在这种情况下 model 并不一定意味着 Eloquent\Model 的实例,而更像是 model模型视图控制器。 Eloquent 只是 ORM 并有一个名为 Model 的类,您可以从该类继承,但模型-class 本身只是业务逻辑的一个实体。它可以扩展 Eloquent,也可以扩展 Doctrine,也可以什么都不扩展。

最后,它只是一个保存您提取的数据的类,例如从数据库,从架构的角度来看,它不知道任何 ORM,它只包含数据。 Book 可能有一个author 属性,甚至可能有一个getAuthor() 方法,但是对于一本书来说,有一个save()find() 方法并没有什么意义。但如果你使用 Eloquent 就可以了。而且没关系,因为方便,在小项目中直接访问也没什么问题。但处理特定 ORM 是存储库(或控制器)的工作,而不是模型的工作。实际模型是 ORM 交互的结果

所以,是的,模型似乎与 Laravel 中的 ORM 如此紧密地绑定可能有点令人困惑,但同样,它对于大多数项目来说非常方便且完美。事实上,你甚至不会注意到它,除非你直接在你的应用程序代码中使用它(例如Book::where(...)->get();)然后决定从 Eloquent 切换到类似 Doctrine 的东西——这显然会破坏你的应用程序。但是,如果这一切都封装在存储库后面,那么当您在数据库甚至 ORM 之间切换时,应用程序的其余部分甚至都不会注意到。

所以,您正在使用存储库,因此只有存储库的 eloquent-implementation 实际上应该知道 Book 还扩展了 Eloquent\Model 并且它可以在其上调用 save() 方法。关键是Book 是否扩展Model 并不(=不应该)重要,它应该仍然可以在您的应用程序的任何地方实例化,因为在您的业务逻辑中它只是一个Book,即普通的旧 PHP 对象,带有一些描述一本书的属性和方法,而不是如何查找或保存对象的策略。这就是存储库的用途。

但是是的,绝对干净的方法是拥有一个BookInterface,然后将其绑定到特定的实现。所以它可能看起来像这样:

接口:


interface BookInterface 
{
    /**
     * Get the ISBN.
     *
     * @return string
     */
    public function getISBN();
}

interface BookRepositoryInterface()
{
    /**
     * Find a book by the given Id.
     *
     * @return null|BookInterface
     */
    public function find($id);
}

具体实现:


class Book extends Model implements BookInterface
{
    public function getISBN()
    {
        return $this->isbn;
    }
}

class EloquentBookRepository implements BookRepositoryInterface
{
    protected $book;

    public function __construct(Model $book)
    {
        $this->book = $book;
    }

    public function find($id)
    {
        return $this->book->find($id);
    }
}

然后将接口绑定到所需的实现:

App::bind('BookInterface', function() 
{
    return new Book;
});

App::bind('BookRepositoryInterface', function() 
{
    return new EloquentBookRepository(new Book);
});

Book 扩展Model 或其他任何东西都没有关系,只要它实现了BookInterface,它就是一本书。这就是为什么我在测试中勇敢地实例化了new Book。因为如果您更改 ORM 并不重要,只有当您有多个 BookInterface 的实现时才重要,但这不太可能(明智的?),我猜。但为了安全起见,既然它已绑定到 IoC-Container,您可以在测试中像这样实例化它:

$book = $this->app->make('BookInterface');

这将返回您当前正在使用的Book 的任何实现的实例。

所以,为了更好的可测试性

  • 代码到接口而不是具体类
  • 使用 Laravel 的 IoC-Container 将接口绑定到具体实现(包括模拟)
  • 使用依赖注入

我希望这是有道理的。

【讨论】:

  • 所以,即使我实例化了一个 Eloquent 子类(在本例中为 $book),它仍然遵循“如果你要为 MongoDB 更改 ORM,你只需要实现一个新的回购类”?
  • @ChristopherFrancisco 不确定我是否完全理解您的问题,但是是的,这就是存储库的用途。您从数据持久层中抽象出来,并提供一种与数据交互的一致方式,无论是在数据库中、文件系统中、通过 Web 服务还是其他方式 - 存储库知道如何获取它,而您的应用程序知道如何获取它询问存储库。理想情况下,您只需为每个模型创建一个 RepositoryInterface,然后在例如实现它一个 EloquentBookRepository、FilesystemBookRepository 或任何你需要的东西。
  • Laravel 实现了控制反转的概念,它允许你在需要某个接口时简单地分配你想要使用的具体类:App::bind('BookRepositoryInterface', new EloquentBookRepository)。然后,您可以将 EloquentBookRepository 替换为例如WebServiceBookRepository 从网络服务而不是从数据库中获取书籍,而无需更改应用程序代码中的任何一行。
  • 我明白了。现在,在testShowBook() 的第一行中实例化一个new Book,我假设它是Eloquent\Model 的子类。这不会使控制器反转的整个交易无效吗?因为如果你改变 ORM,你仍然需要改变 Book,这样它就不会是 Model 的类
  • @ChristopherFrancisco 我已经编辑了我的答案来评论你的问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-30
  • 2016-01-01
相关资源
最近更新 更多