我试图创建单元测试来测试某些特定的类。我使用app()->make()
来实例化要测试的类。实际上,不需要HTTP请求。
然而,一些经过测试的功能需要来自路由参数的信息,因此他们可以拨打电话,例如request()->route()->parameter('info')
,这会引发异常:
在null上调用成员函数parameter()。
我玩过很多次尝试过:
request()->attributes = new \Symfony\Component\HttpFoundation\ParameterBag(['info' => 5]);
request()->route(['info' => 5]);
request()->initialize([], [], ['info' => 5], [], [], [], null);
但他们都没有工作......
如何手动初始化路由器并为其提供一些路由参数?或者只是让request()->route()->parameter()
可用?
class SomeTest extends TestCase
{
public function test_info()
{
$info = request()->route()->parameter('info');
$this->assertEquals($info, 'hello_world');
}
}
否"请求"参与其中。 request()->route()->parameter()
调用实际上位于我的真实代码中的服务提供商中。此测试用例专门用于测试该服务提供商。没有一条路线可以打印该提供商中方法的返回值。
答案 0 :(得分:15)
我假设您需要模拟请求而不实际调度它。有了模拟请求,您需要探测参数值并开发测试用例。
这是一种没有记录的方法。你会感到惊讶!
正如您所知,Laravel的Illuminate\Http\Request
课程建立在Symfony\Component\HttpFoundation\Request
之上。上游类不允许您以setRequestUri()
方式手动设置请求URI。它根据实际的请求标头计算出来。别无他法。
好的,喋喋不休。让我们尝试模拟一个请求:
<?php
use Illuminate\Http\Request;
class ExampleTest extends TestCase
{
public function testBasicExample()
{
$request = new Request([], [], ['info' => 5]);
dd($request->route()->parameter('info'));
}
}
正如你自己提到的,你会得到一个:
错误:在null
上调用成员函数parameter()
Route
为什么?为什么route()
会返回null
?
查看its implementation以及其伴随方法的实现; getRouteResolver()
。 getRouteResolver()
方法返回一个空闭包,然后route()
调用它,因此$route
变量将为null
。然后它返回,因此......错误。
在真实的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
的示例。
所以,现在你不会得到那个错误,因为你实际上有一个请求对象绑定到它的路由。但它还没有成功。如果我们此时运行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
类,看看它的$parameters
property是如何填充的。
当我们bind the request反对该路线时,后续调用bindParameters()
方法会填充$parameters
属性,而bindPathParameters()
方法会调用Symfony's Symfony\Component\Routing\CompiledRoute
来确定路径 - 具体参数(在这种情况下我们没有主机参数)。
该方法将请求的解码路径与prepareRequestUri()
的正则表达式匹配(您可以在上面的转储中看到该正则表达式)并返回作为路径参数的匹配项。如果路径与模式不匹配(这是我们的情况),它将为空。
/**
* 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()
会返回与该模式不匹配的/
。所以无论如何,参数包都是空的。
如果您在decodedPath()
课程上使用Request
方法,那么您将深入研究几种方法,这些方法最终会从http://address.com/#tag {{1} }}。在那种方法中,您可以找到问题的答案。
通过探测一堆HTTP标头来确定请求URI。它首先检查Symfony\Component\HttpFoundation\Request
,然后检查X_ORIGINAL_URL
,然后检查其他几个,最后检查X_REWRITE_URL
标头。您可以将这些标头中的任何一个设置为实际欺骗请求URI,并实现http请求的最小模拟。我们来看看。
REQUEST_URI
令你惊讶的是,它打印出<?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()
特征。
即使绝对不可能像上面的方法那样欺骗请求URI,您也可以部分模拟请求类并设置预期的请求URI。有点像:
SimulatesRequests
这也打印出<?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'));
}
}
。
答案 1 :(得分:1)
我今天使用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;
});
答案 2 :(得分:0)
由于route
被实现为闭包,因此您可以直接在路由中访问route参数,而无需显式调用parameter('info')
。这两个调用返回相同的内容:
$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');
当然要在测试中利用此方法,应该将Request对象注入被测类中,而不要通过request()
方法使用Laravel全局请求对象。
答案 3 :(得分:-1)
使用Laravel phpunit包装器,您可以让测试类扩展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);
}
}