在 Laravel 测试用例中模拟一个 http 请求并解析路由参数
我正在尝试创建单元测试来测试一些特定的类.我使用 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 SymfonyComponentHttpFoundationParameterBag(['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 的 IlluminateHttpRequest
类基于 SymfonyComponentHttpFoundationRequest
.上游类不允许您以 setRequestUri()
方式手动设置请求 URI.它根据实际的请求标头计算出来.别无他法.
As you already know, Laravel's IlluminateHttpRequest
class builds upon SymfonyComponentHttpFoundationRequest
. 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.
好的,闲聊就够了.让我们尝试模拟一个请求:
OK, enough with the chatter. Let's try to simulate a request:
<?php
use IlluminateHttpRequest;
class ExampleTest extends TestCase
{
public function testBasicExample()
{
$request = new Request([], [], ['info' => 5]);
dd($request->route()->parameter('info'));
}
}
正如你自己提到的,你会得到一个:
As you mentioned yourself, you'll get a:
错误:在 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 IlluminateHttpRequest;
use IlluminateRoutingRoute;
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'));
}
}
查看从 Route 的另一个示例" rel="noreferrer">Laravel 自己的RouteCollection
类.
See another example of creating Route
s from Laravel's own RouteCollection
class.
所以,现在你不会得到那个错误,因为你实际上有一个绑定了请求对象的路由.但它还不会起作用.如果我们此时运行 phpunit,我们会得到一个 null
!如果你执行一个 dd($request->route())
你会看到即使它设置了 info
参数名称,它的 parameters
数组为空:
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:
IlluminateRoutingRoute {#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: SymfonyComponentRoutingCompiledRoute {#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
类,看看它的 $parameters
属性 正在填充.
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.
当我们绑定请求 对路由的对象,$parameters
属性由对 bindParameters()
方法,该方法依次调用 bindPathParameters()
找出路径特定的参数(我们没有本例中的主机参数).
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).
该方法将请求的解码路径与 Symfony 的 SymfonyComponentRoutingCompiledRoute
(您也可以在上面的转储中看到该正则表达式)并返回作为路径参数的匹配项.如果路径与模式不匹配(这是我们的情况),它将为空.
That method matches request's decoded path against a regex of Symfony's SymfonyComponentRoutingCompiledRoute
(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 IlluminateHttpRequest $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 SymfonyComponentHttpFoundationRequest
.在那里,正是在这种方法中,您将找到问题的答案.
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 SymfonyComponentHttpFoundationRequest
. 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 请求的最小模拟.让我们看看.
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 IlluminateHttpRequest;
use IlluminateRoutingRoute;
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 IlluminateHttpRequest;
use IlluminateRoutingRoute;
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
.
相关文章