在 Laravel 测试用例中模拟一个 http 请求并解析路由参数

Posted

技术标签:

【中文标题】在 Laravel 测试用例中模拟一个 http 请求并解析路由参数【英文标题】:Simulate a http request and parse route parameters in Laravel testcase 【发布时间】:2017-05-18 14:34:03 【问题描述】:

我正在尝试创建单元测试来测试一些特定的类。我使用app()->make() 来实例化要测试的类。所以实际上,不需要任何 HTTP 请求。

但是,一些经过测试的函数需要来自路由参数的信息,以便它们进行调用,例如request()->route()->parameter('info'),这会引发异常:

在 null 上调用成员函数 parameter()。

我玩了很多,尝试过类似的东西:

request()->attributes = new \Symfony\Component\HttpFoundation\ParameterBag(['info' => 5]);  

request()->route(['info' => 5]);  

request()->initialize([], [], ['info' => 5], [], [], [], null);

但他们都没有工作......

如何手动初始化路由器并为其提供一些路由参数?或者干脆让request()->route()->parameter() 可用?

更新

@Loek:你不理解我。基本上,我在做:

class SomeTest extends TestCase

    public function test_info()
    
        $info = request()->route()->parameter('info');
        $this->assertEquals($info, 'hello_world');
    

不涉及“请求”。 request()->route()->parameter() 调用实际上位于我真实代码中的服务提供者中。该测试用例专门用于测试该服务提供者。没有路由可以打印该提供程序中方法的返回值。

【问题讨论】:

你能在你的服务提供你想要测试的代码吗? @RossWilson 这与这个问题并不相关。举个例子,它可能是一个返回request()->route()->parameter('info')的服务提供者ExpProvider::Info(),我想测试一下。 @RossWilson 但是没有像/test/info/info 这样的路线。在单元测试中,我想打电话给$handler = app()->make(ExProvider::class); $handler->Info();。但在此之前,我该如何设置路由器? 啊,那么你基本上可以模拟你的内核,创建一个请求(字面意思是new Request(),向该请求添加一些参数并触发它。 @Loek 这类似于 Laravel 测试框架中的MakesHttpRequest.php: public function call(...)。它会“模拟”一个“请求”。但是在这个测试用例中没有请求。如果您触发“请求”,它将被路由到端点并返回响应,但显然它与我的问题不同。 【参考方案1】:

我假设您需要模拟一个请求而不实际调度它。有了一个模拟请求,您想探测它的参数值并开发您的测试用例。

有一种未记录的方法可以做到这一点。你会感到惊讶!

问题

如你所知,Laravel 的 Illuminate\Http\Request 类建立在 Symfony\Component\HttpFoundation\Request 之上。上游类不允许您以 setRequestUri() 方式手动设置请求 URI。它根据实际的请求标头计算出来。没有别的办法。

好的,闲聊就够了。让我们尝试模拟一个请求:

<?php

use Illuminate\Http\Request;

class ExampleTest extends TestCase

    public function testBasicExample()
    
        $request = new Request([], [], ['info' => 5]);

        dd($request->route()->parameter('info'));
    

正如你自己提到的,你会得到一个:

错误:在 null 上调用成员函数 parameter()

我们需要一个Route

这是为什么呢?为什么route() 返回null

看看its implementation 以及它的伴随方法的实现; getRouteResolver()getRouteResolver() 方法返回一个空的闭包,然后route() 调用它,所以$route 变量将是null。然后它被返回,因此......错误。

在真正的 HTTP 请求上下文中,Laravel sets up its route resolver,因此您不会收到此类错误。现在您正在模拟请求,您需要自己设置它。让我们看看如何。

<?php

use Illuminate\Http\Request;
use Illuminate\Routing\Route;

class ExampleTest extends TestCase

    public function testBasicExample()
    
        $request = new Request([], [], ['info' => 5]);

        $request->setRouteResolver(function () use ($request) 
            return (new Route('GET', 'testing/info', []))->bind($request);
        );

        dd($request->route()->parameter('info'));
    

查看另一个从Laravel's own RouteCollection class 创建Routes 的示例。

空参数包

所以,现在您不会收到该错误,因为您实际上有一个绑定了请求对象的路由。但它还行不通。如果我们此时运行 phpunit,我们会在脸上看到一个 null!如果您执行dd($request-&gt;route()),您会看到即使设置了info 参数名称,它的parameters 数组也是空的:

Illuminate\Routing\Route #250
  #uri: "testing/info"
  #methods: array:2 [
    0 => "GET"
    1 => "HEAD"
  ]
  #action: array:1 [
    "uses" => null
  ]
  #controller: null
  #defaults: []
  #wheres: []
  #parameters: [] <===================== HERE
  #parameterNames: array:1 [
    0 => "info"
  ]
  #compiled: Symfony\Component\Routing\CompiledRoute #252
    -variables: array:1 [
      0 => "info"
    ]
    -tokens: array:2 [
      0 => array:4 [
        0 => "variable"
        1 => "/"
        2 => "[^/]++"
        3 => "info"
      ]
      1 => array:2 [
        0 => "text"
        1 => "/testing"
      ]
    ]
    -staticPrefix: "/testing"
    -regex: "#^/testing/(?P<info>[^/]++)$#s"
    -pathVariables: array:1 [
      0 => "info"
    ]
    -hostVariables: []
    -hostRegex: null
    -hostTokens: []
  
  #router: null
  #container: null

因此,将 ['info' =&gt; 5] 传递给 Request 构造函数没有任何效果。让我们看一下Route 类,看看它的$parameters property 是如何填充的。

当我们 bind the request 对象路由时,$parameters 属性被随后调用 bindParameters() 方法填充,该方法又调用 bindPathParameters() 以找出特定于路径的参数(我们没有在这种情况下是主机参数)。

该方法将请求的解码路径与Symfony's Symfony\Component\Routing\CompiledRoute 的正则表达式匹配(您也可以在上面的转储中看到该正则表达式)并返回作为路径参数的匹配项。如果路径与模式不匹配(这是我们的情况),它将为空。

/**
 * Get the parameter matches for the path portion of the URI.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return array
 */
protected function bindPathParameters(Request $request)

    preg_match($this->compiled->getRegex(), '/'.$request->decodedPath(), $matches);
    return $matches;

问题在于,当没有实际请求时,$request-&gt;decodedPath() 返回与模式不匹配的/。所以无论如何参数包都是空的。

欺骗请求 URI

如果您遵循Request 类上的decodedPath() 方法,您将深入了解几个方法,这些方法最终将返回来自prepareRequestUri()Symfony\Component\HttpFoundation\Request 的值。在那里,正是在那种方法中,你会找到你问题的答案。

它通过探测一堆 HTTP 标头来确定请求 URI。它首先检查X_ORIGINAL_URL,然后是X_REWRITE_URL,然后是其他几个,最后是REQUEST_URI 标头。您可以将这些标头中的任何一个设置为实际欺骗请求 URI 并实现对 http 请求的最小模拟。让我们看看。

<?php

use Illuminate\Http\Request;
use Illuminate\Routing\Route;

class ExampleTest extends TestCase

    public function testBasicExample()
    
        $request = new Request([], [], [], [], [], ['REQUEST_URI' => 'testing/5']);

        $request->setRouteResolver(function () use ($request) 
            return (new Route('GET', 'testing/info', []))->bind($request);
        );

        dd($request->route()->parameter('info'));
    

令您惊讶的是,它会打印出5info 参数的值。

清理

您可能希望将功能提取到帮助程序 simulateRequest() 方法或 SimulatesRequests 特征中,以便在您的测试用例中使用。

嘲讽

即使绝对不可能像上面的方法那样欺骗请求 URI,您也可以部分模拟请求类并设置您预期的请求 URI。类似的东西:

<?php

use Illuminate\Http\Request;
use Illuminate\Routing\Route;

class ExampleTest extends TestCase


    public function testBasicExample()
    
        $requestMock = Mockery::mock(Request::class)
            ->makePartial()
            ->shouldReceive('path')
            ->once()
            ->andReturn('testing/5');

        app()->instance('request', $requestMock->getMock());

        $request = request();

        $request->setRouteResolver(function () use ($request) 
            return (new Route('GET', 'testing/info', []))->bind($request);
        );

        dd($request->route()->parameter('info'));
    

这也会打印出5

【讨论】:

太棒了!这正是我想要的!谢谢。虽然我们后来发现 Laravel 实际上并没有很好地封装,所以你可以先做一个无关紧要的 $this-&gt;call(),然后再做其他事情(单例对象不会被破坏),但这远不止于此程序化的。我们将来会切换到此实现。 我不确定...我们几乎是偶然注意到的。 非常棒的例子——帮助我做了一些需要路由前缀的模拟。非常感谢。 这个答案很棒!我有最后一个问题/问题。我正在尝试测试我的中间件,它期望 $request-&gt;route('team') 返回一个对象(因为它将感谢路由模型绑定) - 有没有办法“调用”/“触发”路由模型绑定? 这个被接受的答案有很好的解释和很多投票,但我发现@Cranespud 是我测试的更好答案——简短、简单,并且完全符合我需要测试的内容。【参考方案2】:

我今天在使用 Laravel7 时遇到了这个问题,这是我如何解决的,希望它对某人有所帮助

我正在为中间件编写单元测试,它需要检查一些路由参数,所以我正在做的是创建一个固定请求以将其传递给中间件

        $request = Request::create('/api/company/company', 'GET');            
        $request->setRouteResolver(function()  use ($company) 
            $stub = $this->createStub(Route::class);
            $stub->expects($this->any())->method('hasParameter')->with('company')->willReturn(true);
            $stub->expects($this->any())->method('parameter')->with('company')->willReturn($company->id); // not $adminUser's company
            return $stub;
        );

【讨论】:

$request = Request::create('/api/company/company', 'GET'); 为我工作,很好@Cranespud【参考方案3】:

由于route被实现为闭包,你可以直接在路由中访问路由参数,而不需要显式调用parameter('info')。这两个调用返回相同:

$info = $request->route()->parameter('info');
$info = $request->route('info');

第二种方式,可以很容易地模拟 'info' 参数:

$request = $this->createMock(Request::class);
$request->expects($this->once())->method('route')->willReturn('HelloWorld');
$info = $request->route('info');
$this->assertEquals($info, 'HelloWorld');

当然要在你的测试中利用这个方法,你应该在你的测试类中注入 Request 对象,而不是通过 request() 方法使用 Laravel 全局请求对象。

【讨论】:

【参考方案4】:

使用 Laravel phpunit 包装器,您可以让您的测试类扩展 TestCase 并使用 visit() 函数。

如果你想更严格(这在单元测试中可能是一件好事),这种方法真的不推荐。

class UserTest extends TestCase

    /**
     * A basic test example.
     *
     * @return void
     */
    public function testExample()
    
        // This is readable but there's a lot of under-the-hood magic
        $this->visit('/home')
             ->see('Welcome')
             ->seePageIs('/home');

        // You can still be explicit and use phpunit functions
        $this->assertTrue(true);
    

【讨论】:

以上是关于在 Laravel 测试用例中模拟一个 http 请求并解析路由参数的主要内容,如果未能解决你的问题,请参考以下文章

如何模拟酶测试用例中的拖放?

在单元测试用例中模拟 Angular $window

是否可以在 IOS 的 XCTest 单元测试用例中模拟方法(带参数)调用

如何在go中模拟测试用例中结构的方法调用

在快速测试用例中为单例目标 c 类注入依赖项

如何在 iOS Xcode UI 测试用例中启动系统应用