在 Laravel 测试用例中模拟一个 http 请求并解析路由参数

本文介绍了在 Laravel 测试用例中模拟一个 http 请求并解析路由参数的处理方法,对大家解决问题具有一定的参考价值

问题描述

我正在尝试创建单元测试来测试一些特定的类.我使用 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'));
    }
}

查看从 <a href="https://github.com/laravel/framework/blob/5.3/src/Illuminate/Routing/RouteCollection.php#L200 创建 Route 的另一个示例" rel="noreferrer" target="_blank">Laravel 自己的RouteCollection 类.

See another example of creating Routes 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'));
    }
}

出乎你的意料,它打印出5info 参数的值.

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 请求并解析路由参数的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,WP2

admin_action_{$_REQUEST[‘action’]}

do_action( "admin_action_{$_REQUEST[‘action’]}" )动作钩子::在发送“Action”请求变量时激发。Action Hook: Fires when an ‘action’ request variable is sent.目录锚点:#说明#源码说明(Description)钩子名称的动态部分$_REQUEST['action']引用从GET或POST请求派生的操作。源码(Source)更新版本源码位置使用被使用2.6.0 wp-admin/admin.php:...

日期:2020-09-02 17:44:16 浏览:1158

admin_footer-{$GLOBALS[‘hook_suffix’]}

do_action( "admin_footer-{$GLOBALS[‘hook_suffix’]}", string $hook_suffix )操作挂钩:在默认页脚脚本之后打印脚本或数据。Action Hook: Print scripts or data after the default footer scripts.目录锚点:#说明#参数#源码说明(Description)钩子名的动态部分,$GLOBALS['hook_suffix']引用当前页的全局钩子后缀。参数(Parameters)参数类...

日期:2020-09-02 17:44:20 浏览:1060

customize_save_{$this->id_data[‘base’]}

do_action( "customize_save_{$this-&gt;id_data[‘base’]}", WP_Customize_Setting $this )动作钩子::在调用WP_Customize_Setting::save()方法时激发。Action Hook: Fires when the WP_Customize_Setting::save() method is called.目录锚点:#说明#参数#源码说明(Description)钩子名称的动态部分,$this->id_data...

日期:2020-08-15 15:47:24 浏览:798

customize_value_{$this->id_data[‘base’]}

apply_filters( "customize_value_{$this-&gt;id_data[‘base’]}", mixed $default )过滤器::过滤未作为主题模式或选项处理的自定义设置值。Filter Hook: Filter a Customize setting value not handled as a theme_mod or option.目录锚点:#说明#参数#源码说明(Description)钩子名称的动态部分,$this->id_date['base'],指的是设置...

日期:2020-08-15 15:47:24 浏览:885

get_comment_author_url

过滤钩子:过滤评论作者的URL。Filter Hook: Filters the comment author’s URL.目录锚点:#源码源码(Source)更新版本源码位置使用被使用 wp-includes/comment-template.php:32610...

日期:2020-08-10 23:06:14 浏览:925

network_admin_edit_{$_GET[‘action’]}

do_action( "network_admin_edit_{$_GET[‘action’]}" )操作挂钩:启动请求的处理程序操作。Action Hook: Fires the requested handler action.目录锚点:#说明#源码说明(Description)钩子名称的动态部分$u GET['action']引用请求的操作的名称。源码(Source)更新版本源码位置使用被使用3.1.0 wp-admin/network/edit.php:3600...

日期:2020-08-02 09:56:09 浏览:870

network_sites_updated_message_{$_GET[‘updated’]}

apply_filters( "network_sites_updated_message_{$_GET[‘updated’]}", string $msg )筛选器挂钩:在网络管理中筛选特定的非默认站点更新消息。Filter Hook: Filters a specific, non-default site-updated message in the Network admin.目录锚点:#说明#参数#源码说明(Description)钩子名称的动态部分$_GET['updated']引用了非默认的...

日期:2020-08-02 09:56:03 浏览:854

pre_wp_is_site_initialized

过滤器::过滤在访问数据库之前是否初始化站点的检查。Filter Hook: Filters the check for whether a site is initialized before the database is accessed.目录锚点:#源码源码(Source)更新版本源码位置使用被使用 wp-includes/ms-site.php:93910...

日期:2020-07-29 10:15:38 浏览:825

WordPress 的SEO 教学:如何在网站中加入关键字(Meta Keywords)与Meta 描述(Meta Description)?

你想在WordPress 中添加关键字和meta 描述吗?关键字和meta 描述使你能够提高网站的SEO。在本文中,我们将向你展示如何在WordPress 中正确添加关键字和meta 描述。为什么要在WordPress 中添加关键字和Meta 描述?关键字和说明让搜寻引擎更了解您的帖子和页面的内容。关键词是人们寻找您发布的内容时,可能会搜索的重要词语或片语。而Meta Description则是对你的页面和文章的简要描述。如果你想要了解更多关于中继标签的资讯,可以参考Google的说明。Meta 关键字和描...

日期:2020-10-03 21:18:25 浏览:1688

谷歌的SEO是什么

SEO (Search Engine Optimization)中文是搜寻引擎最佳化,意思近于「关键字自然排序」、「网站排名优化」。简言之,SEO是以搜索引擎(如Google、Bing)为曝光媒体的行销手法。例如搜寻「wordpress教学」,会看到本站的「WordPress教学:12个课程…」排行Google第一:关键字:wordpress教学、wordpress课程…若搜寻「网站架设」,则会看到另一个网页排名第1:关键字:网站架设、架站…以上两个网页,每月从搜寻引擎导入自然流量,达2万4千:每月「有机搜...

日期:2020-10-30 17:23:57 浏览:1294