Mocking Request в Laravel 5.1 для (актуального) модульного теста

Прежде всего, мне известно, что в документах говорится:

Примечание: вы не должны имитировать фасад Request. Вместо этого передайте желаемый ввод во вспомогательные методы HTTP, такие как call и post, при запуске теста.

Но такие тесты больше похожи на интеграцию или функциональность, поскольку, даже если вы тестируете контроллер (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
}

Это прекрасно работает, и они следуют стилю Упорядочить, Действовать, Утвердить.

Проблема заключается в 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's 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 с модульными тестами на платформе Laravel, поскольку кажется, что документация ориентирована на интеграционное тестирование путем связывания взаимодействия между зависимостями и SUT с $this->call()?


person Christopher Francisco    schedule 05.08.2015    source источник
comment
Зайдите в это сегодня. По теме: twitter.com/laravelphp/status/556568018864459776   -  person Ravan Scafi    schedule 01.09.2015
comment
Более простой вариант: если ваша тестируемая функция принимает аргумент Request и вам нужен простой запрос пути реального маршрута, тогда: вам не нужно имитировать запрос, вы можете просто создать запрос из маршрута и передать его, например: $myRequest = Request::create('/path/that/I_want', 'GET'); $this->assertTrue(functionUnderTest($myRequest));   -  person Daryn    schedule 15.12.2020


Ответы (3)


Модульное тестирование контроллера при использовании 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, легко выполнить часть процесса аранжировки, что почти позволяет вам читать это как псевдоблочный тест (но это небольшое расширение воображения).

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

person Amo    schedule 25.02.2016
comment
В моем случае я создал вспомогательную функцию, которая использует функцию request () - ›root () для определения результата. например когда URL такой, сделайте то. Итак, я хочу протестировать свою вспомогательную функцию, высмеивая request () - ›root (), чтобы вернуть определенные значения ... но я не могу. Нет ли другого способа правильно имитировать фасад request ()? - person Maccath; 25.02.2016
comment
Я думаю, что если вам нужно издеваться над фасадом, есть возможность для рефакторинга. Для вашей вспомогательной функции я ожидаю, что она примет строковый параметр $url. Таким образом, вы можете легко провести модульное тестирование своей вспомогательной функции, используя любой передаваемый параметр. - person Amo; 25.02.2016

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

person Dylan Buth    schedule 05.08.2015
comment
Да, контроллеры - это клей вашего приложения. Они объединяют многие ваши услуги в действия. Смысл модульного тестирования заключается в изолированном тестировании модулей кода. Контроллеры не должны быть маленьким устройством, они связывают многие вещи вместе. Это гораздо лучший вариант использования для приемочного или функционального теста. - person Dylan Pierce; 05.08.2015
comment
Проблема не только в контроллерах. Сегодня я попробовал функциональный тест для свойства настраиваемого репозитория, который предполагает разбиение результатов на страницы с помощью пагинатора Laravel. Оказалось, что метод Laravel paginate () считывает номер страницы прямо из Request- ›input (), поэтому мне нужно как-то имитировать его, чтобы вернуть правильный номер страницы, как того требует мой тест. - person JustAMartin; 30.09.2015
comment
@JustAMartin Сегодня я столкнулся с точно такой же ситуацией: getPageOfDealers(Request $request) моего репозитория получает объект запроса, затем фильтрует и разбивает результат на страницы соответственно. Но чтобы проверить это, мне нужно издеваться над Request. - person unifreak; 25.10.2017
comment
В качестве обходного пути для моей ситуации я использовал метод Request :: replace для ввода параметров разбивки на страницы. - person JustAMartin; 25.10.2017

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

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

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

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

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

person Ross_102    schedule 25.02.2016