Mocking Request в Laravel 5.1 для (фактического) Unit Test

Прежде всего, я знаю docs:

Примечание. Вы не должны высмеивать фасад запроса. Вместо этого передайте требуемый вход в HTTP-вспомогательные методы, такие как вызов и сообщение при выполнении теста.

Но эти тесты больше похожи на интеграцию или функциональность, поскольку, несмотря на то, что вы тестируете контроллер (SUT), вы не отключаете его из его зависимостей (Request и других, подробнее об этом позже).

Итак, что я делаю, чтобы сделать правильный цикл TDD, высмеивает Repository, Response и Request (с которыми у меня проблемы).

Мой тест выглядит следующим образом:

public function test__it_shows_a_list_of_categories() {
    $categories = [];
    $this->repositoryMock->shouldReceive('getAll')
        ->withNoArgs()
        ->once()
        ->andReturn($categories);
    Response::shouldReceive('view')
        ->once()
        ->with('categories.admin.index')
        ->andReturnSelf();
    Response::shouldReceive('with')
        ->once()
        ->with('categories', $categories)
        ->andReturnSelf();

    $this->sut->index();

    // Assertions as mock expectations
}

Это работает отлично, и они следуют стилю Arrange, Act, Assert.

Проблема заключается в Request, например:

public function test__it_stores_a_category() {
    Redirect::shouldReceive('route')
        ->once()
        ->with('categories.admin.index')
        ->andReturnSelf();

    Request::shouldReceive('only')
        ->once()
        ->with('name')
        ->andReturn(['name' => 'foo']);

    $this->repositoryMock->shouldReceive('create')
        ->once()
        ->with(['name' => 'foo']);

    // Laravel facades wont expose Mockery#getMock() so this is a hackz
    // in order to pass mocked dependency to the controller method
    $this->sut->store(Request::getFacadeRoot());

    // Assertions as mock expectations
}

Как вы можете видеть, я высмеивал вызов Request::only('name'). Но когда я запускаю $ phpunit, я получаю следующую ошибку:

BadMethodCallException: Method Mockery_3_Illuminate_Http_Request::setUserResolver() does not exist on this mock object

Поскольку я не вызываю непосредственно setUserResolver() из моего контроллера, это означает, что он вызывается непосредственно реализацией Request. Но почему? Я высмеивал вызов метода, он не должен вызывать никакой зависимости.

Что я делаю неправильно здесь, почему я получаю это сообщение об ошибке?

PS: В качестве бонуса я рассматриваю это неправильно, заставляя TDD с помощью Unit Tests на основе Laravel, поскольку, похоже, документация ориентирована на интеграционное тестирование путем взаимодействия взаимодействия между зависимостями и SUT с помощью $this->call()?

Ответ 1

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

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

Поскольку все запросы, ответы и другие классы полностью протестированы (либо через лежащий в основе класс Symfony, либо сам Laravel), как разработчик я буду заниматься только тестированием кода, который у меня есть.

Я бы написал приемочное испытание.

<?php

use App\User;
use App\Page;
use App\Template;
use App\PageType;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class CategoryControllerTest extends TestCase
{
    use DatabaseTransactions;

    /** @test */
    public function test__it_shows_a_paginated_list_of_categories()
    {
        // Arrange
        $categories = factory(Category::class, 30)->create();

        // Act
        $this->visit('/categories')

        // Assert
            ->see('Total categories: 30')
            // Additional assertions to verify the right categories can be seen may be a useful additional test
            ->seePageIs('/categories')
            ->click('Next')
            ->seePageIs('/categories?page=2')
            ->click('Previous')
            ->seePageIs('/categories?page=1');
    }

}

Поскольку в этом тесте используется признак DatabaseTransactions, легко выполнить аранжировочную часть процесса, что почти позволяет вам прочитать это как псевдо-unit test (но это небольшое растяжение воображения).

Самое главное, этот тест подтверждает, что мои ожидания выполнены. Мой тест называется test_it_shows_a_paginated_list_of_categories, и моя версия теста делает именно это. Я чувствую, что маршрут unit test только утверждает, что вызывается куча методов, но никогда не проверяет, что я показываю список данных категорий на странице.

Ответ 2

У вас всегда будут проблемы с правильными unit test контроллерами. Я рекомендую приемочное тестирование их с чем-то вроде Codeception. Используя приемочные тесты, вы можете убедиться, что ваши контроллеры/представления обрабатывают любые данные соответствующим образом.

Ответ 3

Я пробовал издеваться над запросом на свои тесты тоже без успеха. Вот как я проверяю, сохранен ли элемент:

public function test__it_stores_a_category() {
    $this->action(
            'POST',
            '[email protected]',
            [],
            [
                'name' => 'foo',
            ]
        );

    $this->assertRedirectedTo('categories/admin/index');

    $this->seeInDatabase('categories', ['name' => 'foo']);
}

Надеюсь, что это поможет.