問題描述
我正在嘗試創建單元測試來測試一些特定的類.我使用 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
.
這篇關于在 Laravel 測試用例中模擬一個 http 請求并解析路由參數的文章就介紹到這了,希望我們推薦的答案對大家有所幫助,也希望大家多多支持html5模板網!