因此,您正在尝试对控制器进行单元测试,以确保使用预期的参数调用正确的方法。控制器方法从存储库中获取模型并将其传递给视图。所以我们必须确保
- 在 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 将接口绑定到具体实现(包括模拟)
- 使用依赖注入
我希望这是有道理的。