在 Laravel 测试用例中模拟一个 http 请求并解析路由参数
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/41461497/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Simulate a http request and parse route parameters in Laravel testcase
提问by Frederick Zhang
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.
我正在尝试创建单元测试来测试一些特定的类。我app()->make()
用来实例化要测试的类。所以实际上,不需要 HTTP 请求。
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:
然而,一些被测试的函数需要来自路由参数的信息,所以它们会调用 eg request()->route()->parameter('info')
,这会引发异常:
Call to a member function parameter() on null.
在 null 上调用成员函数 parameter()。
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...
但他们都没有工作......
How could I manually initialize the router and feed some routing parameters to it? Or simply make request()->route()->parameter()
available?
如何手动初始化路由器并向其提供一些路由参数?还是简单地request()->route()->parameter()
提供?
Update
更新
@Loek: You didn't understand me. Basically, I'm doing:
@Loek:你没有理解我。基本上,我在做:
class SomeTest extends TestCase
{
public function test_info()
{
$info = request()->route()->parameter('info');
$this->assertEquals($info, 'hello_world');
}
}
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.
不涉及“请求”。request()->route()->parameter()
在我的真实代码中,调用实际上位于服务提供者中。此测试用例专门用于测试该服务提供者。没有一个路由可以打印该提供程序中方法的返回值。
回答by sepehr
I assume you need to simulatea 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!
有一种未记录的方法可以做到这一点。你会感到惊讶!
The problem
问题
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.
正如你已经知道的,Laravel 的Illuminate\Http\Request
类建立在Symfony\Component\HttpFoundation\Request
. 上游类不允许您以某种setRequestUri()
方式手动设置请求 URI 。它根据实际的请求标头计算出来。没有其他办法。
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'));
}
}
As you mentioned yourself, you'll get a:
正如你自己提到的,你会得到一个:
Error: Call to a member function parameter() on null
错误:在 null 上调用成员函数 parameter()
We need a Route
我们需要一个 Route
Why is that? Why route()
returns null
?
这是为什么?为什么route()
返回null
?
Have a look at its implementationas 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.
看看它的实现以及它的伴随方法的实现;getRouteResolver()
. 该getRouteResolver()
方法返回一个空闭包,然后route()
调用它,因此$route
变量将为null
. 然后它被返回,因此......错误。
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.
在真实的 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'));
}
}
See another example of creating Route
s from Laravel's own RouteCollection
class.
请参阅Route
从Laravel 自己的RouteCollection
class创建s 的另一个示例。
Empty parameters bag
空参数包
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:
所以,现在你不会得到那个错误,因为你实际上有一个绑定了请求对象的路由。但它还不会起作用。如果我们此时运行 phpunit,我们将得到一个结果null
!如果你执行 add($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
}
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
propertyis getting populated.
所以将它传递['info' => 5]
给Request
构造函数没有任何效果。让我们看看这个Route
类,看看它的$parameters
属性是如何填充的。
When we bind the requestobject 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).
当我们将请求对象绑定到路由时,该$parameters
属性会通过对该bindParameters()
方法的后续调用来填充,该方法又会调用bindPathParameters()
以找出特定于路径的参数(在这种情况下我们没有主机参数)。
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).
该方法将请求的解码路径与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;
}
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()
返回的/
内容与模式不匹配。所以参数包将是空的,无论如何。
Spoofing the request URI
欺骗请求URI
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.
如果您decodedPath()
在Request
类上遵循该方法,您将深入了解几个方法,这些方法最终会从prepareRequestUri()
of返回一个值Symfony\Component\HttpFoundation\Request
。在那里,正是在这种方法中,您将找到问题的答案。
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 spoofthe request URI and achieve minimumsimulation of a http request. Let's see.
它通过探测一堆 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'));
}
}
To your surprise, it prints out 5
; the value of info
parameter.
出乎你的意料,它打印出来5
;info
参数的值。
Cleanup
清理
You might want to extract the functionality to a helper simulateRequest()
method, or a SimulatesRequests
trait which can be used across your test cases.
您可能希望将功能提取到辅助simulateRequest()
方法或SimulatesRequests
可在您的测试用例中使用的特征。
Mocking
嘲讽
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:
即使绝对不可能像上述方法那样欺骗请求 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'));
}
}
This prints out 5
as well.
这也打印出来5
。
回答by T30
Since route
is implemented as a closure, you can access a route parameter directly in the route, without explicitly calling parameter('info')
. These two calls returns the same:
由于route
是作为闭包实现的,因此您可以直接在路由中访问路由参数,而无需显式调用parameter('info')
. 这两个调用返回相同的:
$info = $request->route()->parameter('info');
$info = $request->route('info');
The second way, makes mocking the 'info' parameter very easy:
第二种方式,使模拟 'info' 参数非常容易:
$request = $this->createMock(Request::class);
$request->expects($this->once())->method('route')->willReturn('HelloWorld');
$info = $request->route('info');
$this->assertEquals($info, 'HelloWorld');
Of course to exploit this method in your tests, you should inject the Request object in your class under test, instead of using the Laravel global request object through the request()
method.
当然要在你的测试中利用这个方法,你应该在你的被测类中注入 Request 对象,而不是通过该request()
方法使用 Laravel 全局请求对象。
回答by Cranespud
I ran into this problem today using Laravel7 here is how I solved it, hope it helps somebody
我今天使用 Laravel7 遇到了这个问题,这是我解决的方法,希望它可以帮助某人
I'm writing unit tests for a middleware, it needs to check for some route parameters, so what I'm doing is creating a fixed request to pass it to the middleware
我正在为一个中间件编写单元测试,它需要检查一些路由参数,所以我正在做的是创建一个固定的请求将它传递给中间件
$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;
});
回答by Loek
Using the Laravel phpunit wrapper, you can let your test class extend TestCase
and use the visit()
function.
使用 Laravel phpunit 包装器,您可以让您的测试类扩展TestCase
并使用该visit()
函数。
If you want to be stricter (which in unit testing is probably a good thing), this method isn't really recommended.
如果您想更严格(这在单元测试中可能是一件好事),则不建议使用此方法。
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);
}
}