Тестирование вызова связанного метода в 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.


person Dwight    schedule 13.02.2014    source источник
comment
Прочтите документацию github.com/padraic/.   -  person Shakil    schedule 13.02.2014


Ответы (3)


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

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

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

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

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

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

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

В любом случае, это всего лишь моя копейка.

person alexrussell    schedule 13.02.2014
comment
Также я знаю, что это не отвечает на заданный вопрос напрямую, но я думаю, что это хороший ответ, поскольку он отвечает на более широкий вопрос вашего кода и полезен для сообщества в целом. - person alexrussell; 13.02.2014
comment
Да, это отличный ответ. На самом деле, прочитав это, я почувствовал себя немного глупо, потому что теперь это кажется гораздо более очевидным; проверьте, что конечный результат соответствует ожиданиям, и проверяйте более высокие уровни кода только в том случае, если они критически важны для этого процесса. Я оставлю свой ответ ниже, так как технически он достигает результата, который я изначально искал, но это неправильный подход. - person Dwight; 13.02.2014
comment
Вместе мы ответим на обе стороны вопроса :) - person alexrussell; 13.02.2014
comment
Вместо прямого использования моделей в вашем контроллере используйте класс репозитория и убедитесь, что контроллер вызывает его должным образом: culttt.com/2013/07/08/ Затем следует протестировать класс репозитория с помощью интеграционного теста (а не с помощью модульный тест по причинам, объясненным @alexrussell в его ответе). - person cafonso; 29.04.2015

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

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

$this->mock->shouldReceive('remember->with->notSold->take->orderBy->get')
    ->andRe‌​turn($this->collection);
person petercoles    schedule 16.03.2014
comment
это работает, но я заметил, что это приводит к сбою покрытия кода (если вы его используете) - person dwenaus; 17.03.2014
comment
Я этого не знал, так что спасибо за указание. Еще одна причина не засасывать во внутренности тестируемого агрегата :) - person petercoles; 18.03.2014
comment
@petercoles 3 года спустя, но $this-›mock недоступен. Как создать экземпляр? - person Mehrdad Shokri; 02.03.2018
comment
OP устанавливает $this-›mock в конструкторе в своем (слегка неправильно названном) TextbooksControllerTest, вероятно, чтобы избежать повторного выполнения этого в методе setUp, хотя лично я предпочел бы последнее. - person petercoles; 08.03.2018

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

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

$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);
person Dwight    schedule 13.02.2014