Laravel PHPUnit 模拟请求

Posted

技术标签:

【中文标题】Laravel PHPUnit 模拟请求【英文标题】:Laravel PHPUnit mock Request 【发布时间】:2018-03-03 15:39:14 【问题描述】:

我正在控制器上执行 phpUnit,但我似乎无法正确模拟请求。

这是控制器:

use Illuminate\Http\Request;

public function insert(Request $request)

    // ... some codes here
    if ($request->has('username')) 
        $userEmail = $request->get('username');
     else if ($request->has('email')) 
        $userEmail = $request->get('email');
    
    // ... some codes here

然后进行单元测试,

public function testIndex()

    // ... some codes here

    $requestParams = [
        'username' => 'test',
        'email'    => 'test@test.com'
    ];

    $request = $this->getMockBuilder('Illuminate\Http\Request')
        ->disableOriginalConstructor()
        ->setMethods(['getMethod', 'retrieveItem', 'getRealMethod', 'all', 'getInputSource', 'get', 'has'])
        ->getMock();

    $request->expects($this->any())
        ->method('get')
        ->willReturn($requestParams);

    $request->expects($this->any())
        ->method('has')
        ->willReturn($requestParams);

    $request->expects($this->any())
        ->method('all')
        ->willReturn($requestParams);

    // ... some codes here

这里的问题是,每当我 var_dump($request->has('username'); 它总是返回 $requestParams 值,其中是整个数组。我希望它应该返回true,因为数组中存在用户名键。

然后,当我删除 $requestParams 上的用户名键时,它应该返回 false,因为它不包含数组上的 username

【问题讨论】:

【参考方案1】:

模拟请求并不理想,但有时你只是想这样做:

protected function createRequest(
    $method,
    $content,
    $uri = '/test',
    $server = ['CONTENT_TYPE' => 'application/json'],
    $parameters = [],
    $cookies = [],
    $files = []
) 
    $request = new \Illuminate\Http\Request;
    return $request->createFromBase(
        \Symfony\Component\HttpFoundation\Request::create(
            $uri,
            $method,
            $parameters,
            $cookies,
            $files,
            $server,
            $content
        )
    );

【讨论】:

【参考方案2】:

据我所见和理解,您是在告诉您的单元测试,当您在请求对象上调用 $request->has() 时,它应该返回 $requestParams 数组,而不是 true 或 false,或其他任何东西.

除非您使用方法调用专门检查发送的内容,否则您的模拟实际上并不关心发送的内容,它只关心它是否被调用。

如果在您的用例中可行,您可能想要探索创建一个空请求并用数据填充它,因为这样可以让您更轻松地运行单元测试,并且问题更少。这并非在所有情况下都有效。

您可以在单元测试中包含您所做的断言,这样我们就可以更清楚地看到您遇到的问题,但事实就是如此。它完全返回您告诉它返回的内容。即使这不是您真正希望它返回的内容。

Mocks 用于将单元测试与系统的其余部分分开。因此,您通常倾向于只检查是否调用了特定方法,以查看您的代码是否实际退出到您模拟的类以及它是否具有您要发送的预期数据。在某些极端情况下,您可能想要模拟您实际测试的系统,但这通常表明您的代码过于依赖其他类或它做的太多。

使用模拟的另一个原因是在方法调用中满足类型转换约束。在这些情况下,您通常会创建一个空的模拟对象,并在其中填充一些您的代码将接受或中断以测试代码的虚拟数据。

在您的情况下,您似乎想检查您的代码是否真正正常工作,为此我建议不要模拟请求,或者在您告诉它返回 true 或 false 的地方进行特定测试(测试两种​​情况)

大致如下:

$request->expects($this->any())
    ->method('has')
    ->with('username')
    ->willReturn(true); // or false in your next test

编辑: 正如您在下面的评论中提到的,您遇到了您在代码中多次使用 has 方法并遇到问题的问题。

我在回复评论中链接到的问题更详细,但总而言之,您可以使用内联函数或 at() 方法来处理多种情况。

使用 at(),您可以提供代码的特定迭代以仅命中测试的那部分。已经提到过,这会使您的测试变得相当脆弱,因为之前添加的任何测试都会破坏测试。

$request->expects($this->at(0))
    ->method('has')
    ->with('username')
    ->willReturn('returnValue');

$request->expects($this->at(1))
    ->method('has')
    ->with('email')
    ->willReturn('otherReturnValue');

内联函数(回调)解决方案允许您自定义测试以允许多种情况并根据需要返回数据。不幸的是,我对这个概念不太熟悉,因为我自己以前没有使用过它。我建议阅读PHPUnit docs 了解更多信息。

最后,我仍然建议不要模拟请求,而是发出一个空请求,您将使用要检查的数据填充。 Laravel 提供了一些令人印象深刻的方法,这些方法可以让您手动填充请求,其中包含您通常会测试的大量数据。

例如,您可以使用

添加数据(发布/获取数据)
request->add(['fieldname' => 'value'])

作为最后几点,我想提一下,您似乎使用了 var_dump。 Laravel 有两个自己的函数,它们相似并且在调试中非常有用。 您可以使用dd();dump(); dd(); 转储并停止执行代码,而 dump(); 只输出您决定的任何内容。所以你可以做dd($request);dump($request); 看看变量/类对象/等有什么。它甚至会使用一些 javascript 等将其置于相当漂亮的布局中,以便您查看其中的内容等。如果您不知道它的存在,可能想检查一下。

【讨论】:

感谢@Tropus。 $request->has('username') 现在返回 true$request->has('email') 返回错误:Expectation failed for method name is equal to <string:has> when invoked zero or more times Parameter 0 for invocation Illuminate\Http\Request::has('email') does not match expected value. Failed asserting that two strings are equal.。我为email 添加了另一个sn-p,就像您所说的一样,但仍然出现错误。 @basagabi 第一种情况你说你期望一个特定的方法是领先的,你第二次调用它会推迟到第一次定义。在这种情况下,您需要在 with 语句中使用回调,或者使用 at() 而不是 any() 例如:expects($this->at(0)) 用于第一次出现,expects($this->at(1)) 用于第二次出现。这些出色的答案here和here中的更多详细信息【参考方案3】:

如果您的情况更简单,比@Ian 更简单的答案:

根据https://***.com/a/61903688/135114, 如果

    您的被测函数采用$request 参数,并且 您不需要对请求做一些时髦的事情 - 真正的路由路径对您来说已经足够好了

...那么您不需要“模拟”请求(例如,嘲弄), 你可以创建一个Request并传递它,例如

public function test_myFunc_condition_expectedResult() 
    ...
    $mockRequest = Request::create('/path/that/I_want', 'GET'); 
    $this->assertTrue($myClass->myFuncThat($mockRequest));

【讨论】:

【参考方案4】:

如果您使用request()->user(),您可以设置用户解析器。它允许您返回您想要的用户。我遇到了同样的问题,我的解决方案是这样的:

public function testSomething()

    $user = User::factory()->create();

    request()->setUserResolver(function() use ($user) 
        return $user;
    );
    
    // Dumped result will be newly created $user
    dd(request()->user());

【讨论】:

以上是关于Laravel PHPUnit 模拟请求的主要内容,如果未能解决你的问题,请参考以下文章

Laravel / PHPUnit:运行测试时没有模拟类

使用 Laravel 和 PHPUnit 模拟函数

如何用 phpunit 模拟 Laravel Eloquent 模型?

PHPUnit 问题断言模拟是使用 Laravel 的 ReflectionProperty 对象

如何在 PHPUnit / Laravel 中模拟 JSON 文件

用于 PHPunit 的 Laravel 5.4 模型模拟