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'),这会引发一个异常:

Call to a member function parameter() on null.

我已经玩了很多次了,try 了以下方法:

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?

使现代化

@Loek:你不理解我.基本上,我在做:

class SomeTest extends TestCase
{
    public function test_info()
    {
        $info = request()->route()->parameter('info');
        $this->assertEquals($info, 'hello_world');
    }
}

不涉及"请求".在我的真实代码中,request()->route()->parameter()呼叫实际上位于服务Provider 中.此测试用例专门用于测试该服务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.

有一种没有记录的方法可以做到这一点.你会惊讶的!

问题

正如你已经知道的,拉威尔的Illuminate\Http\Request门课是在Symfony\Component\HttpFoundation\Request门课的基础上建立起来的.upstream 类不允许您以手动方式设置请求URI.它根据实际的请求头来计算.别无 Select .

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

We need a 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.

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 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的另一个示例.

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:

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()返回与模式不匹配的/.所以参数包无论如何都会是空的.

Spoofing the request 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.

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 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参数的值.

Cleanup

您可能希望将功能提取到帮助器simulateRequest()方法,或者可以跨测试用例使用的SimulatesRequests特征.

Mocking

即使完全不可能像上面的方法那样欺骗请求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.

Laravel相关问答推荐

为什么在Blade 文件中输出用户通知时出现错误?

Laravel:通过数据透视表数据限制多对多Eager 加载

Laravel中如何动态更改路由链接?

如何让 Laravel 的 Collection 表现得像一个流?

验证判断请求的值是否存在于另一个表 Laravel 9 中

1 子编译中的警告(使用'stats.children:true'和'--stats-children'了解更多详细信息)

如何为 CMP 横幅安装谷歌同意脚本?

Laravel 连接表

用户''@'localhost'对数据库'forge'的访问被拒绝随机出现

查询 Laravel Select WhereIn 数组

Laravel 5.4 存储:下载文件.文件不存在,但显然存在

Laravel 5.x 中 onUpdate / onDelete 的可用操作

使用'with'时,Laravel belongsTo 返回 null

如何在 Eloquent 上设置条件关系

Laravel - 自定义时间戳列名称

如何从不是控制器方法的方法发送响应?

Laravel 5 Carbon 全局语言环境

使用 Nginx 设置 Laravel

在 Laravel 5 中找不到类App\Http\Controllers\admin\Auth

连接到 tcp://smtp.mail.yahoo.com:465 超时