在Laravel测试用例中模拟一个http请求并解析路由参数
Simulate a http request and parse route parameters in Laravel testcase
我正在尝试创建单元测试来测试某些特定的 类。我使用 app()->make()
实例化 类 进行测试。所以实际上,不需要 HTTP 请求。
但是,一些经过测试的函数需要来自路由参数的信息,以便它们进行调用,例如request()->route()->parameter('info')
,这会引发异常:
Call to a member function parameter() on null.
我玩了很多,尝试过类似的东西:
request()->attributes = new \Symfony\Component\HttpFoundation\ParameterBag(['info' => 5]);
request()->route(['info' => 5]);
request()->initialize([], [], ['info' => 5], [], [], [], null);
但其中 none 有效...
如何手动初始化路由器并向其提供一些路由参数?或者只是让 request()->route()->parameter()
可用?
更新
@Loek:你没听懂我的意思。基本上,我在做:
class SomeTest extends TestCase
{
public function test_info()
{
$info = request()->route()->parameter('info');
$this->assertEquals($info, 'hello_world');
}
}
不涉及“请求”。 request()->route()->parameter()
调用实际上位于我的真实代码中的服务提供者中。这个测试用例专门用来测试那个服务提供者。没有路由会打印该提供程序中方法的返回值。
使用 Laravel phpunit 包装器,您可以让您的测试 class 扩展 TestCase
并使用 visit()
函数。
如果您想更严格(这在单元测试中可能是一件好事),则不推荐使用此方法。
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);
}
}
我假设您需要 模拟 一个请求而不实际发送它。有了模拟请求,您想要探测它的参数值并开发您的测试用例。
有一种未记录的方法可以做到这一点。你会感到惊讶!
问题
如您所知,Laravel 的 Illuminate\Http\Request
class builds upon Symfony\Component\HttpFoundation\Request
。上游 class 不允许您以 setRequestUri()
的方式手动设置请求 URI。它根据实际请求 headers 计算出来。别无他法。
好了,废话不多说了。让我们尝试模拟一个请求:
<?php
use Illuminate\Http\Request;
class ExampleTest extends TestCase
{
public function testBasicExample()
{
$request = new Request([], [], ['info' => 5]);
dd($request->route()->parameter('info'));
}
}
正如您自己提到的,您会得到:
Error: Call to a member function parameter() on null
我们需要一个Route
这是为什么?为什么 route()
returns null
?
看看its implementation as well as the implementation of its companion method; getRouteResolver()
。 getRouteResolver()
方法 return 是一个空闭包,然后 route()
调用它,因此 $route
变量将是 null
。然后它得到 returned,因此...错误。
在真实的 HTTP 请求上下文中,Laravel sets up its route resolver,所以你不会得到这样的错误。现在您正在模拟请求,您需要自己设置它。让我们看看如何。
<?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'));
}
}
查看另一个从 Laravel's own RouteCollection
class 创建 Route
的示例。
清空参数包
所以,现在您不会收到该错误,因为您实际上有一个路由,请求 object 绑定到它。但它还行不通。如果此时我们 运行 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
}
因此将 ['info' => 5]
传递给 Request
构造函数没有任何效果。让我们看一下 Route
class,看看它的 $parameters
property 是如何填充的。
当我们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()
找出path-specific参数时(在这种情况下我们没有主机参数)。
该方法将请求的解码路径与 Symfony's Symfony\Component\Routing\CompiledRoute
的正则表达式相匹配(您也可以在上面的转储中看到该正则表达式)和 returns 作为路径参数的匹配项。如果路径与模式不匹配(这是我们的情况),它将为空。
/**
* 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()
returns /
与模式不匹配。所以无论如何参数包都会是空的。
欺骗请求 URI
如果您在 Request
class 上遵循 decodedPath()
方法,您将深入了解几个方法,最终 return 来自 prepareRequestUri()
共 Symfony\Component\HttpFoundation\Request
。在那里,正是在那种方法中,你会找到问题的答案。
它通过探测一堆 HTTP headers 来找出请求 URI。它首先检查 X_ORIGINAL_URL
,然后是 X_REWRITE_URL
,然后是其他几个,最后是 REQUEST_URI
header。您可以将这些 header 中的任何一个设置为实际 spoof 请求 URI 并实现对 http 请求的 minimum 模拟。让我们看看。
<?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
参数的值。
清理
您可能希望将功能提取到辅助 simulateRequest()
方法或 SimulatesRequests
特征中,以便在您的测试用例中使用。
嘲讽
即使完全不可能像上面的方法那样欺骗请求 URI,您也可以部分模拟请求 class 并设置您期望的请求 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'));
}
}
这也打印出 5
。
由于 route
是作为闭包实现的,您可以直接在路由中访问路由参数,而无需显式调用 parameter('info')
。这两个调用returns相同:
$info = $request->route()->parameter('info');
$info = $request->route('info');
第二种方法,使模拟 'info' 参数变得非常容易:
$request = $this->createMock(Request::class);
$request->expects($this->once())->method('route')->willReturn('HelloWorld');
$info = $request->route('info');
$this->assertEquals($info, 'HelloWorld');
当然要在你的测试中利用这个方法,你应该在你的 class 测试中注入 Request 对象,而不是通过 request()
使用 Laravel 全局请求对象方法。
我 运行 今天使用 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;
});
我正在尝试创建单元测试来测试某些特定的 类。我使用 app()->make()
实例化 类 进行测试。所以实际上,不需要 HTTP 请求。
但是,一些经过测试的函数需要来自路由参数的信息,以便它们进行调用,例如request()->route()->parameter('info')
,这会引发异常:
Call to a member function parameter() on null.
我玩了很多,尝试过类似的东西:
request()->attributes = new \Symfony\Component\HttpFoundation\ParameterBag(['info' => 5]);
request()->route(['info' => 5]);
request()->initialize([], [], ['info' => 5], [], [], [], null);
但其中 none 有效...
如何手动初始化路由器并向其提供一些路由参数?或者只是让 request()->route()->parameter()
可用?
更新
@Loek:你没听懂我的意思。基本上,我在做:
class SomeTest extends TestCase
{
public function test_info()
{
$info = request()->route()->parameter('info');
$this->assertEquals($info, 'hello_world');
}
}
不涉及“请求”。 request()->route()->parameter()
调用实际上位于我的真实代码中的服务提供者中。这个测试用例专门用来测试那个服务提供者。没有路由会打印该提供程序中方法的返回值。
使用 Laravel phpunit 包装器,您可以让您的测试 class 扩展 TestCase
并使用 visit()
函数。
如果您想更严格(这在单元测试中可能是一件好事),则不推荐使用此方法。
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);
}
}
我假设您需要 模拟 一个请求而不实际发送它。有了模拟请求,您想要探测它的参数值并开发您的测试用例。
有一种未记录的方法可以做到这一点。你会感到惊讶!
问题
如您所知,Laravel 的 Illuminate\Http\Request
class builds upon Symfony\Component\HttpFoundation\Request
。上游 class 不允许您以 setRequestUri()
的方式手动设置请求 URI。它根据实际请求 headers 计算出来。别无他法。
好了,废话不多说了。让我们尝试模拟一个请求:
<?php
use Illuminate\Http\Request;
class ExampleTest extends TestCase
{
public function testBasicExample()
{
$request = new Request([], [], ['info' => 5]);
dd($request->route()->parameter('info'));
}
}
正如您自己提到的,您会得到:
Error: Call to a member function parameter() on null
我们需要一个Route
这是为什么?为什么 route()
returns null
?
看看its implementation as well as the implementation of its companion method; getRouteResolver()
。 getRouteResolver()
方法 return 是一个空闭包,然后 route()
调用它,因此 $route
变量将是 null
。然后它得到 returned,因此...错误。
在真实的 HTTP 请求上下文中,Laravel sets up its route resolver,所以你不会得到这样的错误。现在您正在模拟请求,您需要自己设置它。让我们看看如何。
<?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'));
}
}
查看另一个从 Laravel's own RouteCollection
class 创建 Route
的示例。
清空参数包
所以,现在您不会收到该错误,因为您实际上有一个路由,请求 object 绑定到它。但它还行不通。如果此时我们 运行 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
}
因此将 ['info' => 5]
传递给 Request
构造函数没有任何效果。让我们看一下 Route
class,看看它的 $parameters
property 是如何填充的。
当我们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()
找出path-specific参数时(在这种情况下我们没有主机参数)。
该方法将请求的解码路径与 Symfony's Symfony\Component\Routing\CompiledRoute
的正则表达式相匹配(您也可以在上面的转储中看到该正则表达式)和 returns 作为路径参数的匹配项。如果路径与模式不匹配(这是我们的情况),它将为空。
/**
* 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()
returns /
与模式不匹配。所以无论如何参数包都会是空的。
欺骗请求 URI
如果您在 Request
class 上遵循 decodedPath()
方法,您将深入了解几个方法,最终 return 来自 prepareRequestUri()
共 Symfony\Component\HttpFoundation\Request
。在那里,正是在那种方法中,你会找到问题的答案。
它通过探测一堆 HTTP headers 来找出请求 URI。它首先检查 X_ORIGINAL_URL
,然后是 X_REWRITE_URL
,然后是其他几个,最后是 REQUEST_URI
header。您可以将这些 header 中的任何一个设置为实际 spoof 请求 URI 并实现对 http 请求的 minimum 模拟。让我们看看。
<?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
参数的值。
清理
您可能希望将功能提取到辅助 simulateRequest()
方法或 SimulatesRequests
特征中,以便在您的测试用例中使用。
嘲讽
即使完全不可能像上面的方法那样欺骗请求 URI,您也可以部分模拟请求 class 并设置您期望的请求 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'));
}
}
这也打印出 5
。
由于 route
是作为闭包实现的,您可以直接在路由中访问路由参数,而无需显式调用 parameter('info')
。这两个调用returns相同:
$info = $request->route()->parameter('info');
$info = $request->route('info');
第二种方法,使模拟 'info' 参数变得非常容易:
$request = $this->createMock(Request::class);
$request->expects($this->once())->method('route')->willReturn('HelloWorld');
$info = $request->route('info');
$this->assertEquals($info, 'HelloWorld');
当然要在你的测试中利用这个方法,你应该在你的 class 测试中注入 Request 对象,而不是通过 request()
使用 Laravel 全局请求对象方法。
我 运行 今天使用 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;
});