在 Laravel 测试用例中模拟一个 http 请求并解析路由参数
Posted
技术标签:
【中文标题】在 Laravel 测试用例中模拟一个 http 请求并解析路由参数【英文标题】:Simulate a http request and parse route parameters in Laravel testcase 【发布时间】:2017-05-18 14:34:03 【问题描述】:我正在尝试创建单元测试来测试一些特定的类。我使用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()
可用?
更新
@Loek:你不理解我。基本上,我在做:
class SomeTest extends TestCase
public function test_info()
$info = request()->route()->parameter('info');
$this->assertEquals($info, 'hello_world');
不涉及“请求”。 request()->route()->parameter()
调用实际上位于我真实代码中的服务提供者中。该测试用例专门用于测试该服务提供者。没有路由可以打印该提供程序中方法的返回值。
【问题讨论】:
你能在你的服务提供你想要测试的代码吗? @RossWilson 这与这个问题并不相关。举个例子,它可能是一个返回request()->route()->parameter('info')
的服务提供者ExpProvider::Info()
,我想测试一下。
@RossWilson 但是没有像/test/info/info
这样的路线。在单元测试中,我想打电话给$handler = app()->make(ExProvider::class); $handler->Info();
。但在此之前,我该如何设置路由器?
啊,那么你基本上可以模拟你的内核,创建一个请求(字面意思是new Request()
,向该请求添加一些参数并触发它。
@Loek 这类似于 Laravel 测试框架中的MakesHttpRequest.php: public function call(...)
。它会“模拟”一个“请求”。但是在这个测试用例中没有请求。如果您触发“请求”,它将被路由到端点并返回响应,但显然它与我的问题不同。
【参考方案1】:
我假设您需要模拟一个请求而不实际调度它。有了一个模拟请求,您想探测它的参数值并开发您的测试用例。
有一种未记录的方法可以做到这一点。你会感到惊讶!
问题
如你所知,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
s 的示例。
空参数包
所以,现在您不会收到该错误,因为您实际上有一个绑定了请求对象的路由。但它还行不通。如果我们此时运行 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 对象路由时,$parameters
属性被随后调用 bindParameters()
方法填充,该方法又调用 bindPathParameters()
以找出特定于路径的参数(我们没有在这种情况下是主机参数)。
该方法将请求的解码路径与Symfony's 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;
问题在于,当没有实际请求时,$request->decodedPath()
返回与模式不匹配的/
。所以无论如何参数包都是空的。
欺骗请求 URI
如果您遵循Request
类上的decodedPath()
方法,您将深入了解几个方法,这些方法最终将返回来自prepareRequestUri()
的Symfony\Component\HttpFoundation\Request
的值。在那里,正是在那种方法中,你会找到你问题的答案。
它通过探测一堆 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'));
令您惊讶的是,它会打印出5
; info
参数的值。
清理
您可能希望将功能提取到帮助程序 simulateRequest()
方法或 SimulatesRequests
特征中,以便在您的测试用例中使用。
嘲讽
即使绝对不可能像上面的方法那样欺骗请求 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'));
这也会打印出5
。
【讨论】:
太棒了!这正是我想要的!谢谢。虽然我们后来发现 Laravel 实际上并没有很好地封装,所以你可以先做一个无关紧要的$this->call()
,然后再做其他事情(单例对象不会被破坏),但这远不止于此程序化的。我们将来会切换到此实现。
我不确定...我们几乎是偶然注意到的。
非常棒的例子——帮助我做了一些需要路由前缀的模拟。非常感谢。
这个答案很棒!我有最后一个问题/问题。我正在尝试测试我的中间件,它期望 $request->route('team')
返回一个对象(因为它将感谢路由模型绑定) - 有没有办法“调用”/“触发”路由模型绑定?
这个被接受的答案有很好的解释和很多投票,但我发现@Cranespud 是我测试的更好答案——简短、简单,并且完全符合我需要测试的内容。【参考方案2】:
我今天在使用 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;
);
【讨论】:
$request = Request::create('/api/company/company', 'GET');
为我工作,很好@Cranespud【参考方案3】:
由于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 全局请求对象。
【讨论】:
【参考方案4】:使用 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);
【讨论】:
以上是关于在 Laravel 测试用例中模拟一个 http 请求并解析路由参数的主要内容,如果未能解决你的问题,请参考以下文章