Laravel Tests传递给模型到View

时间:2015-04-15 14:51:01

标签: unit-testing laravel laravel-5 mockery

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

我猜我应该嘲笑返回的雄辩模型,但我发现了2个问题:

  1. 如果我最终会嘲笑雄辩的模型,那么实现存储库模式有什么意义呢
  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('')
    );
    
  3. 修改

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

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

    问题是,在控制器的末尾,它返回一个视图:

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

    因此,测试用例也会运行view方法,如果没有模拟$book,则为null并崩溃

1 个答案:

答案 0 :(得分:3)

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

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

但首先要做的事情是:

  

如果我最终会嘲笑雄辩的模型,那么实施存储库模式的重点是什么?

除了(可测试的)通过不同的来源,(可测试的)集中式缓存策略等来实现数据访问规则之外还有很多其他用途。在这种情况下,您不会测试存储库而您实际上甚至不会关心什么回来了,你只是对某些方法被调用感兴趣。因此,结合依赖注入的概念,您现在拥有了一个强大的工具:您只需使用模拟切换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的类

嗯......是的,不是。是的,我直接在测试中实例化了模型类,但在此上下文中的模型并不一定意味着Eloquent\Model的实例,但更像是模型 - 视图 - 控制器中的模型。 Eloquent只是ORM并且有一个你继承的名为Model的类,但是model- class 本身只是业务逻辑的一个实体。它可以扩展Eloquent,它可以扩展Doctrine,或者它根本不会扩展。

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

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

因此,您正在使用存储库,因此只有存储库的雄辩实现才能真正意识到Book::where(...)->get();还扩展了Book并且它可以调用Eloquent\Model方法就可以了。关键是,如果save()扩展Book,它不会(=不应该),它应该仍然可以在应用程序的任何位置实例化,因为在您的业务逻辑中它和&#t}} #39; s只是一个Model,即一个普通的旧PHP对象,其中包含描述书籍的一些属性和方法,而不是如何查找或保留对象的策略。这就是存储库的用途。

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

<强>接口


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的原因。因为如果你改变ORM并不重要,那么只有你有new Book的几个实现才有意义,但我认为这不太可能(明智吗?)。但是为了安全起见,现在它已经绑定到IoC-Container,你可以在测试中像这样实例化它:

BookInterface

将返回您当前正在使用的$book = $this->app->make('BookInterface'); 的任何实现的实例。

因此,为了更好的可测试性

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

我希望这是有道理的。