Laravel 5.1 中针对(实际)单元测试的模拟请求

Mocking Request in Laravel 5.1 for (actual) Unit Test

首先,我知道 docs 状态:

Note: You should not mock the Request facade. Instead, pass the input you desire into the HTTP helper methods such as call and post when running your test.

但是那些类型的测试更像是集成或功能因为即使你正在测试一个控制器SUT),你没有将它从它的依赖项中分离出来(Request 和其他人,稍后会详细介绍)。

所以我正在做的是模拟 RepositoryResponseRequest(我有问题与).

我的测试是这样的:

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'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:作为奖励,我是否通过在 Laravel 框架上强制使用单元测试进行 TDD 来看待它,因为看起来文档是通过耦合交互来面向集成测试的$this->call()?

的依赖项和 SUT

在尝试正确地对控制器进行单元测试时,您总是会 运行 遇到问题。我建议使用 Codeception 之类的方法对它们进行验收测试。使用验收测试,您可以确保您的 controllers/views 正确处理任何数据。

使用 Laravel 时对控制器进行单元测试似乎不是一个好主意。鉴于控制器的上下文,我不会关心在请求、响应甚至存储库 classes 上调用的各个方法。

此外,因为您想将测试对象与其依赖项分离而对属于框架的控制器进行单元测试是没有意义的,因为您只能在具有那些给定依赖项的框架中使用该控制器。

由于请求、响应和其他 classes 都经过了全面测试(通过底层 Symfony class 或 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 特性,所以很容易执行过程中的 arrange 部分,这几乎可以让你把它当作一个伪单元测试来阅读(但这有点夸张了).

最重要的是,这个测试证实了我的期望得到满足。我的测试称为 test_it_shows_a_paginated_list_of_categories,我的测试版本就是这样做的。我觉得单元测试路线只断言调用了一堆方法,但从未验证我在页面上显示了给定类别的列表。

我也尝试模拟我的测试请求,但没有成功。 以下是我测试项目是否已保存的方法:

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

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

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

希望对您有所帮助