我正在正确地模拟我的存储库,但在像show()
这样的情况下,它会返回null
,因此视图最终会因为调用null对象上的属性而导致测试崩溃
我猜我应该嘲笑返回的雄辩模型,但我发现了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
并崩溃
答案 0 :(得分:3)
因此,您尝试对控制器进行单元测试,以确保使用预期参数调用正确的方法。 controller-method从repo中获取模型并将其传递给视图。所以我们必须确保
find()
- 方法但首先要做的事情是:
如果我最终会嘲笑雄辩的模型,那么实施存储库模式的重点是什么?
除了(可测试的)通过不同的来源,(可测试的)集中式缓存策略等来实现数据访问规则之外还有很多其他用途。在这种情况下,您不会测试存储库而您实际上甚至不会关心什么回来了,你只是对某些方法被调用感兴趣。因此,结合依赖注入的概念,您现在拥有了一个强大的工具:您只需使用模拟切换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');
的任何实现的实例。
因此,为了更好的可测试性
我希望这是有道理的。