模拟一个http请求并在Laravel测试用例中解析路由参数 [英] Simulate a http request and parse route parameters in Laravel testcase

查看:54
本文介绍了模拟一个http请求并在Laravel测试用例中解析路由参数的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试创建单元测试来测试某些特定的类.我使用 app()-> make()实例化要测试的类.因此,实际上,不需要HTTP请求.

I'm trying to create unit tests to test some specific classes. I use app()->make() to instantiate the classes to test. So actually, no HTTP requests are needed.

但是,某些经过测试的功能需要路由参数中的信息,因此它们会进行调用,例如 request()-> route()-> parameter('info'),这会引发异常:

However, some of the tested functions need information from the routing parameters so they'll make calls e.g. request()->route()->parameter('info'), and this throws an exception:

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

Call to a member function parameter() on null.

我玩了很多次,尝试过类似的事情:

I've played around a lot and tried something like:

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

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

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

但是他们都不起作用...

but none of them worked...

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

How could I manually initialize the router and feed some routing parameters to it? Or simply make request()->route()->parameter() available?

@Loek:您不了解我.基本上,我在做:

@Loek: You didn't understand me. Basically, I'm doing:

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

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

No "requests" involved. The request()->route()->parameter() call is actually located in a service provider in my real code. This test case is specifically used to test that service provider. There isn't a route which will print the returning value from the methods in that provider.

推荐答案

我假设您需要模拟一个请求,而无需实际分派.在适当的模拟请求下,您希望对其进行探测以获取参数值并开发测试用例.

I assume you need to simulate a request without actually dispatching it. With a simulated request in place, you want to probe it for parameter values and develop your testcase.

有一个未记录的方法可以做到这一点.您会感到惊讶!

There's an undocumented way to do this. You'll be surprised!

您已经知道,Laravel的 Illuminate\ Http \ Request 类基于 Symfony \ Component \ HttpFoundation \ Request .上游类不允许您以 setRequestUri()的方式手动设置请求URI.它根据实际的请求标头进行计算.没有其他办法.

As you already know, Laravel's Illuminate\Http\Request class builds upon Symfony\Component\HttpFoundation\Request. The upstream class does not allow you to setup a request URI manually in a setRequestUri() way. It figures it out based on the actual request headers. No other way around.

好的,with不休.让我们尝试模拟一个请求:

OK, enough with the chatter. Let's try to simulate a request:

<?php

use Illuminate\Http\Request;

class ExampleTest extends TestCase
{
    public function testBasicExample()
    {
        $request = new Request([], [], ['info' => 5]);

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

如您所言,您将获得:

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

Error: Call to a member function parameter() on null

我们需要一个 Route

那是为什么?为什么 route()返回 null ?

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

Have a look at its implementation as well as the implementation of its companion method; getRouteResolver(). The getRouteResolver() method returns an empty closure, then route() calls it and so the $route variable will be null. Then it gets returned and thus... the error.

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

In a real HTTP request context, Laravel sets up its route resolver, so you won't get such errors. Now that you're simulating the request, you need to set up that by yourself. Let's see how.

<?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'));
    }
}

请参阅从因此,现在您将不会收到该错误,因为您实际上具有绑定了请求对象的路由.但这还行不通.如果我们此时运行phpunit,我们将得到 null 的表情!如果执行 dd($ request-> route()),即使设置了 info 参数名称,您也会看到其参数数组为空:

So, now you won't get that error because you actually have a route with the request object bound to it. But it won't work yet. If we run phpunit at this point, we'll get a null in the face! If you do a dd($request->route()) you'll see that even though it has the info parameter name set up, its parameters array is empty:

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'=>5] Request 构造函数没有任何作用.让我们看一下 Route 类,看看它的

So passing that ['info' => 5] to Request constructor has no effect whatsoever. Let's have a look at the Route class and see how its $parameters property is getting populated.

当我们绑定请求路由对象,随后调用

When we bind the request object to the route, the $parameters property gets populated by a subsequent call to the bindParameters() method which in turn calls bindPathParameters() to figure out path-specific parameters (we don't have a host parameter in this case).

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

That method matches request's decoded path against a regex of Symfony's Symfony\Component\Routing\CompiledRoute (You can see that regex in the above dump as well) and returns the matches which are path parameters. It will be empty if the path doesn't match the pattern (which is our case).

/**
 * 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-> decodedPath()返回与模式不匹配的/.因此,无论如何,参数包都将为空.

The problem is that when there's no actual request, that $request->decodedPath() returns / which does not match the pattern. So the parameters bag will be empty, no matter what.

如果您遵循 Request 类上的 decodedPath()方法,则将深入研究几种方法,这些方法最终将从 prepareRequestUri() of Symfony \ Component \ HttpFoundation \ Request .在那里,正是通过这种方法,您将找到问题的答案.

If you follow that decodedPath() method on the Request class, you'll go deep through a couple of methods which will finally return a value from prepareRequestUri() of Symfony\Component\HttpFoundation\Request. There, exactly in that method, you'll find the answer to your question.

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

It's figuring out the request URI by probing a bunch of HTTP headers. It first checks for X_ORIGINAL_URL, then X_REWRITE_URL, then a few others and finally for the REQUEST_URI header. You can set either of these headers to actually spoof the request URI and achieve minimum simulation of a http request. Let's see.

<?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'));
    }
}

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

To your surprise, it prints out 5; the value of info parameter.

您可能希望将功能提取到帮助程序 simulateRequest()方法或可以在整个测试用例中使用的 SimulatesRequests 特性.

You might want to extract the functionality to a helper simulateRequest() method, or a SimulatesRequests trait which can be used across your test cases.

即使像上述方法那样绝对不可能欺骗请求URI,也可以部分模拟请求类并设置所需的请求URI.类似于:

Even if it was absolutely impossible to spoof the request URI like the approach above, you could partially mock the request class and set your expected request URI. Something along the lines of:

<?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 .

这篇关于模拟一个http请求并在Laravel测试用例中解析路由参数的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆