【问题标题】:Mocking Request in Laravel 5.1 for (actual) Unit TestLaravel 5.1 中用于(实际)单元测试的模拟请求
【发布时间】:2015-08-05 02:41:08
【问题描述】:

首先,我知道docs 状态:

注意:你不应该模拟 Request 门面。相反,在运行测试时将所需的输入传递给 HTTP 帮助方法,例如 call 和 post。

但是这些测试更像是集成或功能,因为即使您正在测试控制器SUT) ,你并没有将它与它的依赖关系解耦(Request 和其他人,稍后会详细介绍)。

所以,为了执行正确的TDD 循环,我正在做的是模拟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 和单元测试来寻找错误的方式,因为文档似乎是通过将依赖项和 SUT 之间的交互与 $this->call() 耦合来进行集成测试的?

【问题讨论】:

  • 今天遇到了这个问题。相关:twitter.com/laravelphp/status/556568018864459776
  • 更简单的选项:如果您的被测函数采用Request 参数,并且您只需要一个简单的“真实路由路径”请求,那么:您不需要mock一个请求,你可以创建一个请求从路由并传递它,像这样:$myRequest = Request::create('/path/that/I_want', 'GET'); $this->assertTrue(functionUnderTest($myRequest));

标签: unit-testing laravel laravel-5 tdd laravel-5.1


【解决方案1】:

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

此外,对属于框架的控制器进行单元测试是没有意义的,因为您希望将测试 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 trait,所以很容易执行这个过程的排列部分,这几乎可以让你把它当作一个伪单元测试来阅读(但这只是想象力的延伸)。

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

【讨论】:

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

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

【讨论】:

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

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

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

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

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

希望对你有帮助

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-28
    • 2018-10-17
    • 2019-06-25
    • 1970-01-01
    • 2022-01-26
    • 1970-01-01
    相关资源
    最近更新 更多