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

Fre*_*ang 10 php phpunit unit-testing laravel laravel-5.3

我正在尝试创建单元测试来测试某些特定的类.我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);
Run Code Online (Sandbox Code Playgroud)

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

如何手动初始化路由器并向其提供一些路由参数?或者只是request()->route()->parameter()提供?

更新

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

class SomeTest extends TestCase
{
    public function test_info()
    {
        $info = request()->route()->parameter('info');
        $this->assertEquals($info, 'hello_world');
    }
}
Run Code Online (Sandbox Code Playgroud)

没有涉及"请求".该request()->route()->parameter()呼叫实际上位于我的真实代码中的服务提供商中.此测试用例专门用于测试该服务提供商.没有路由将从该提供程序中的方法打印返回值.

sep*_*ehr 21

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

有一种无证的方法可以做到这一点.你会感到惊讶!

问题

正如您所知,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'));
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

我们需要一个 Route

这是为什么?为何route()回归null

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

在真实的HTTP请求上下文中,Laravel设置其路由解析器,因此您不会遇到此类错误.现在您正在模拟请求,您需要自己设置.我们来看看如何.

<?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'));
    }
}
Run Code Online (Sandbox Code Playgroud)

请参阅另一个RouteLaravel自己的RouteCollection创建s的示例.

空参数包

所以,现在你不会得到那个错误,因为你实际上有一个请求对象绑定到它的路由.但它还不行.如果我们此时运行phpunit,我们将获得一个null面子!如果你这样做,dd($request->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
}
Run Code Online (Sandbox Code Playgroud)

所以将它传递['info' => 5]Request构造函数没有任何效果.让我们看看这个Route类,看看它的$parameters属性是如何填充的.

当我们将请求对象绑定到路由时,该$parameters属性将通过对该bindParameters()方法的后续调用来填充,该方法又调用bindPathParameters()以找出特定于路径的参数(在这种情况下我们没有主机参数).

该方法将请求的解码路径与Symfony的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;
}
Run Code Online (Sandbox Code Playgroud)

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

欺骗请求URI

如果你decodedPath()Request类上遵循该方法,你将深入研究几个最终将返回值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'));
    }
}
Run Code Online (Sandbox Code Playgroud)

令你惊讶的是,它打印出来5; info参数的值.

清理

您可能希望将功能提取到辅助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'));
    }
}
Run Code Online (Sandbox Code Playgroud)

这也打印出来5.

  • 了不起!这正是我想要的!谢谢。虽然我们后来发现 Laravel 实际上并没有做得很好,所以你可以先做一个无关的什么鬼 `$this-&gt;call()`,然后再做其他事情(单例对象不会被破坏) ),这更加程序化。我们将来会切换到这种实现。 (2认同)

Cra*_*pud 7

我今天使用 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;
        });
Run Code Online (Sandbox Code Playgroud)