Тестирование прикомандированного вызова метода в Mockery

Я пытаюсь правильно высмеивать прикованный вызов к модели Eloquent в контроллере. В моем контроллере я использую инъекцию зависимостей для доступа к модели, чтобы ее было легко высмеять, однако я не уверен, как тестировать прикованные вызовы и заставить работать правильно. Это все в Laravel 4.1 с использованием PHPUnit и Mockery.

контроллер:

<?php class TextbooksController extends BaseController { protected $textbook; public function __construct(Textbook $textbook) { $this->textbook = $textbook; } public function index() { $textbooks = $this->textbook->remember(5) ->with('user') ->notSold() ->take(25) ->orderBy('created_at', 'desc') ->get(); return View::make('textbooks.index', compact('textbooks')); } } 

Тест контроллера:

 <?php class TextbooksControllerText extends TestCase { public function __construct() { $this->mock = Mockery::mock('Eloquent', 'Textbook'); } public function tearDown() { Mockery::close(); } public function testIndex() { // Here I want properly mock my chained call to the Textbook // model. $this->action('GET', 'TextbooksController@index'); $this->assertResponseOk(); $this->assertViewHas('textbooks'); } } 

Я пытался достичь этого, поставив этот код перед вызовом $this->action() в тесте.

 $this->mock->shouldReceive('remember')->with(5)->once(); $this->mock->shouldReceive('with')->with('user')->once(); $this->mock->shouldReceive('notSold')->once(); $this->app->instance('Textbook', $this->mock); 

Однако это приводит к ошибке Fatal error: Call to a member function with() on a non-object in /app/controllers/TextbooksController.php on line 28 .

Я также попробовал цепную альтернативу, надеясь, что это сделает трюк.

 $this->mock->shouldReceive('remember')->with(5)->once() ->shouldReceive('with')->with('user')->once() ->shouldReceive('notSold')->once(); $this->app->instance('Textbook', $this->mock); 

Каков наилучший подход, который я должен предпринять для тестирования этого вызова с цепным методом с помощью Mockery.

Я совершенно новичок в тестировании себя, и весь этот ответ может быть ошибочным в глазах большинства людей, но я вижу, как люди часто тестируют неправильную вещь. Если вы проверите точно все, что делает метод, то вы не тестируете, а просто пишите метод дважды.

Вы должны думать о своем коде как о чём-то черном ящике – не думайте знать, что происходит внутри, когда вы пишете свои тесты. Вызовите метод с заданным входом, ожидайте выход. Иногда вам нужно убедиться в том, что некоторые другие эффекты произошли, и именно тогда приходит материал shouldReceive. Но опять же это более высокий уровень, чем это тестирование цепочки коллекции – вы должны проверить, что код для выполнения этого кода делает, но именно так происходит сам код. Таким образом, цепочка сбора должна быть каким-то образом извлечена каким-либо другим способом, и вы должны просто проверить, вызван ли этот метод.

Чем больше вы тестируете фактический письменный код (а не цель кода), тем больше проблем у вас будет. Например, если вам нужно обновить код, чтобы сделать то же самое по-другому (возможно, remember(6) не remember(5) как часть этой цепочки или что-то в этом роде), вам также необходимо обновить свой тест, чтобы убедиться, что вы remember(6) теперь называется, когда вы не должны его вообще тестировать.

Разумеется, этот совет не просто подходит для цепных методов, ведь в любое время вы гарантируете, что различные объекты будут называть их различными методами при тестировании данного метода.

Насколько мне не нравится термин «красный, зеленый, рефактор», вы должны рассмотреть его здесь, поскольку есть два момента, когда ваш метод тестирования не работает:

  • Красный / зеленый: когда вы впервые пишете неудачный тест, ваш код не должен иметь все эти shouldReceive s (возможно, один или два, если это имеет смысл, см. Выше) – если это так, то вы не пишете тест, но вы пишете код. И действительно, это показатель того, что вы сначала написали код, а затем тест, чтобы он соответствовал коду, что противоречит тестовому TDD.
  • refactor: Предполагая, что вы сначала написали код, а затем тест, чтобы он соответствовал коду (или, как ни странно, удалось точно угадать, что должно появиться, чтобы написать в вашем тесте, что код просто волшебным образом разработан). Это плохо, но скажем, вы сделали это, так как это не конец света. Теперь вам нужно рефакторировать, но вы не можете без изменения теста. Вы тестируете так тесно связан с кодом, что любой рефакторинг нарушит тест. То есть, опять же, против идеи TDD.

Даже если вы не следуете тест-первой TDD, вы должны хотя бы понять, что шаг рефакторинга должен выполняться без нарушения ваших тестов.

Во всяком случае, это просто моя таппанс.

Первоначально комментарий, но перешел к ответу, чтобы сделать код разборчивым!

Я склоняюсь к ответу @ alexrussell , хотя средний уровень был бы таким:

 $this->mock->shouldReceive('remember->with->notSold->take->orderBy->get') ->andRe‌​turn($this->collection); 

Я обнаружил эту технику, но мне это не нравится. Это очень много. Я думаю, что для достижения этого должен быть более чистый / более простой метод.

В конструкторе:

 $this->collection = Mockery::mock('Illuminate\Database\Eloquent\Collection')->shouldDeferMissing(); 

В тесте:

 $this->mock->shouldReceive('remember')->with(5)->andReturn($this->mock); $this->mock->shouldReceive('with')->with('user')->andReturn($this->mock); $this->mock->shouldReceive('notSold')->andReturn($this->mock); $this->mock->shouldReceive('take')->with(25)->andReturn($this->mock); $this->mock->shouldReceive('orderBy')->with('created_at', 'DESC')->andReturn($this->mock); $this->mock->shouldReceive('get')->andReturn($this->collection);