在Laravel测试用例

时间:2017-01-04 10:24:47

标签: php laravel unit-testing phpunit laravel-5.3

我试图创建单元测试来测试某些特定的类。我使用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()调用实际上位于我的真实代码中的服务提供商中。此测试用例专门用于测试该服务提供商。没有一条路线可以打印该提供商中方法的返回值。

4 个答案:

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

欺骗请求URI

如果您在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);
    }
}