问题 用于(实际)单元测试的Laravel 5.1中的模拟请求


首先,我知道了 文档 状态:

注意:您不应该模拟请求外观。相反,在运行测试时,将您想要的输入传递给HTTP帮助程序方法,例如调用和发布。

 那种测试更像是 整合或功能 因为即使你正在测试一个 调节器 (该 SUT),你没有将它与它的依赖性脱钩(Request 和其他人,稍后更多关于这一点)。

所以我正在做的,为了做正确的事 TDD 循环,嘲笑 RepositoryResponse 和 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:作为奖励,我通过在Laravel框架上使用单元测试强制TDD来查看它是错误的方式,因为它似乎通过耦合依赖关系和SUT之间的交互来进行集成测试。 $this->call()


1331
2017-08-05 02:41


起源

今天进入这个。有关: twitter.com/laravelphp/status/556568018864459776 - Ravan


答案:


使用Laravel时对控制器进行单元测试似乎不是一个好主意。在给定Controller的上下文的情况下,我不会关注在Request,Response甚至存储库类上调用的各个方法。

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

由于请求,响应和其他类都经过了全面测试(通过底层的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 我的测试版本确实如此。我觉得单元测试路由只断言调用了一堆方法,但是从来没有验证我是否在页面上显示给定类别的列表。


7
2018-02-25 13:56



我的个人用例是我创建了一个帮助函数,它使用request() - > root()函数来确定结果。例如当URL为此时,执行此操作。所以,我想通过模拟request() - > root()来测试我的帮助函数,以返回特定的值...但我不能。有没有其他方法来正确模拟request()外观? - Maccath
我想如果你不得不嘲笑Facade,就有机会进行重构。对于你的辅助函数,我希望它接受一个字符串参数 $url。这样,您可以使用要传递的任何参数轻松地对辅助函数进行单元测试。 - Amo


尝试正确地对测试控制器进行单元化时,您总会遇到问题。我建议用类似的方式验收它们 Codeception。使用验收测试可以确保您的控制器/视图正确处理任何数据。


3
2017-08-05 14:15



是的,控制器就像是应用程序的“粘合剂”。他们将您的许多服务结合到一起。单元测试的目的是单独测试代码单元。控制器并不是一个小单元,它们将许多东西捆绑在一起。对于验收或功能测试来说,这是一个更好的用例。 - Dylan Pierce
问题不仅在于控制器。今天我尝试了一个自定义存储库特性的功能测试,它通过Laravel的分页器调用分页结果。事实证明,Laravel的paginate()方法直接从Request-> input()读取页码,所以我必须以某种方式模拟它以返回正确的页码,这是我的测试所要求的。 - JustAMartin
@JustAMartin我今天遇到了完全相同的情况,我的存储库 getPageOfDealers(Request $request) 收到一个Request对象,然后相应地过滤和分页结果。但要测试这个我必须模拟Request。 - UniFreak
作为我的情况的解决方法,我使用Request :: replace方法来注入分页参数。 - JustAMartin


我也试图嘲笑我的测试请求但没有成功。 以下是测试项目是否保存的方法:

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

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

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

希望它有所帮助


1
2018-02-25 15:44