2014-02-13 2 views
6

Я пытаюсь должным образом высмеять прикованный вызов к модели Eloquent в контроллере. В моем контроллере я использую инъекцию зависимостей для доступа к модели, чтобы ее было легко высмеять, однако я не уверен, как тестировать прикованные вызовы и заставить работать правильно. Это все в Laravel 4.1 с использованием PHPUnit и Mockery.Тестирование прикомандированного вызова метода в 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', '[email protected]'); 

     $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.

+0

Пожалуйста, прочитайте документацию https://github.com/padraic/mockery#mocking-demeter-chains-and-fluent -interfaces – Shakil

ответ

3

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

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

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

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

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

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

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

В любом случае, это только моя таппа.

+0

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

+1

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

+0

Вместе мы отвечаем на обе стороны вопроса :) – alexrussell

1

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

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

$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); 
15

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

Я склоняюсь к @alexrussell's answer тоже, хотя середина будет:

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

это работает, но я заметил, что это приводит к сбою кода (если вы используете это) – dwenaus

+0

Я этого не знал, поэтому спасибо, что указали это. Еще одна причина не всасывать внутренности тестируемого устройства :) – petercoles

+0

@petercoles 3 года спустя, но $ this-> mock недоступен. Как создать экземпляр? – Mehrdad

Смежные вопросы