在 Laravel 中使用 PHPUnit 测试 HTTP 动词时遇到问题

Trouble testing HTTP verbs with PHPUnit in Laravel

在开始之前,我想说我已经看过 as well as 。不要让名字骗了你;他们是2个不同的职位。这两个帖子都超过 9 个月,没有好的答案。

我在测试路线时遇到了一些麻烦。因此,我在 Mac Mini 运行 OS X Yosemite 10.10.2 上创建了一个新的 Laravel 5 项目。我正在制作 API。所以,我首先开始制作 API 的后端部分。我决定我想使用 PHPUnit 来为这个项目实现 TDD。我的 PHPUnit 版本是 4.0.20。我是 运行 PHP 5.5.14,我在 artisan 命令行工具中使用 Laravel 打包的服务器。

现在,我开始确保我可以连接到服务器。所以,我的第一个测试看起来像:

public function testGetMessagesResponse()
{
    $response = $this->call('GET', 'Messages');

    $this->assertEquals(200, $response->getStatusCode());
}

这一切顺利。但是,当我转到 POST 请求时,它失败了。这是我的第二次测试的样子:

public function testPostMessagesResponse()
{
    $response = $this->call('POST', 'Messages');

    $this->assertEquals(200, $response->getStatusCode());
}

这个测试失败了。输出如下所示:

1) MessagesTest::testPostMessagesResponse
Failed asserting that 500 matches expected 200.

在我的 routes.php 文件中,我注册了这条路线:

Route::resource('Messages', 'MessagesController');

在我的 MessagesController 中,我拥有所有必需的方法:

<?php namespace App\Http\Controllers;

use App\Http\Requests;
use App\Http\Controllers\Controller;

use Illuminate\Http\Request;

class MessagesController extends Controller {

    /**
     * Display a listing of the resource.
     *
     * @return Response
     */
    public function index()
    {
        return 'response';
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return Response
     */
    public function create()
    {
        return 'response';
    }

    /**
     * Store a newly created resource in storage.
     *
     * @return Response
     */
    public function store()
    {
        return 'response';
    }

    /**
    * Display the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function show($id)
    {
        return 'response';
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function edit($id)
    {
        return 'response';
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  int  $id
     * @return Response
     */
    public function update($id)
    {
        return 'response';
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return Response
     */
    public function destroy($id)
    {
        return 'response';
    }

}

所以,暂时不管 HTTP 动词,我应该得到 'response'。现在,我决定用 curl 来验证测试:

curl -X POST http://myapp.com/Messages

此 returns Laravel 'Oops!' 页。所以确实存在 一些 类服务器错误,但我不确定要寻找什么。应用程序流程看起来不错,但显然行不通。这是我的 Laravel 日志中的堆栈跟踪:

[timestamp] local.ERROR: exception 'Illuminate\Session\TokenMismatchException' in /Users/username/Sites/project/storage/framework/compiled.php:2375
Stack trace:
#0 /Users/username/Sites/project/app/Http/Middleware/VerifyCsrfToken.php(17): Illuminate\Foundation\Http\Middleware\VerifyCsrfToken->handle(Object(Illuminate\Http\Request), Object(Closure))
#1 /Users/username/Sites/project/storage/framework/compiled.php(8858): App\Http\Middleware\VerifyCsrfToken->handle(Object(Illuminate\Http\Request), Object(Closure))
#2 /Users/username/Sites/project/storage/framework/compiled.php(11990): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(Illuminate\Http\Request))
#3 /Users/username/Sites/project/storage/framework/compiled.php(8858): Illuminate\View\Middleware\ShareErrorsFromSession->handle(Object(Illuminate\Http\Request), Object(Closure))
#4 /Users/username/Sites/project/storage/framework/compiled.php(10696): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(Illuminate\Http\Request))
#5 /Users/username/Sites/project/storage/framework/compiled.php(8858): Illuminate\Session\Middleware\StartSession->handle(Object(Illuminate\Http\Request), Object(Closure))
#6 /Users/username/Sites/project/storage/framework/compiled.php(11696): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(Illuminate\Http\Request))
#7 /Users/username/Sites/project/storage/framework/compiled.php(8858): Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse->handle(Object(Illuminate\Http\Request), Object(Closure))
#8 /Users/username/Sites/project/storage/framework/compiled.php(11645): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(Illuminate\Http\Request))
#9 /Users/username/Sites/project/storage/framework/compiled.php(8858): Illuminate\Cookie\Middleware\EncryptCookies->handle(Object(Illuminate\Http\Request), Object(Closure))
#10 /Users/username/Sites/project/storage/framework/compiled.php(2411): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(Illuminate\Http\Request))
#11 /Users/username/Sites/project/storage/framework/compiled.php(8858): Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode->handle(Object(Illuminate\Http\Request), Object(Closure))
#12 [internal function]: Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(Illuminate\Http\Request))
#13 /Users/username/Sites/project/storage/framework/compiled.php(8849): call_user_func(Object(Closure), Object(Illuminate\Http\Request))
#14 /Users/username/Sites/project/storage/framework/compiled.php(1862): Illuminate\Pipeline\Pipeline->then(Object(Closure))
#15 /Users/username/Sites/project/storage/framework/compiled.php(1852): Illuminate\Foundation\Http\Kernel->sendRequestThroughRouter(Object(Illuminate\Http\Request))
#16 /Users/username/Sites/project/public/index.php(53): Illuminate\Foundation\Http\Kernel->handle(Object(Illuminate\Http\Request))
#17 /Users/username/Sites/project/server.php(21): require_once('/Users/username/Site...')
#18 {main}

我是 Laravel 的新手,所以我不确定如何真正深入研究并使用堆栈跟踪找出问题所在。我也在没有 Route::resource 的情况下尝试过,但这并没有缓解我的情况。我的 GET 仍然工作正常 (Route::get),但我的 POST (Route::post) 仍然出现相同的 500 错误。 .../compiled.php 中包含第 2375 行的函数(来自堆栈跟踪顶部抛出的异常)由 Laravel 生成,如下所示:

public function handle($request, Closure $next)
{
    if ($this->isReading($request) || $this->tokensMatch($request)) {
        return $this->addCookieToResponse($request, $next($request));
    }
    throw new TokenMismatchException();
}

那时,我试图编辑 POST 测试,因为我实际上并没有发送任何数据。所以这是我修改后的第二次测试:

public function testPostMessagesResponse()
{
    $response = $this->call('POST', 'Messages', array("key" => "value"));

    $this->assertEquals(200, $response->getStatusCode());
}

我收到同样的错误信息。我觉得我缺少一些简单的东西,但我无法弄清楚。任何帮助将不胜感激。

你可以在调用堆栈的第一个函数中看到你的问题

#0 /Users/username/Sites/project/app/Http/Middleware/VerifyCsrfToken.php(17): Illuminate\Foundation\Http\Middleware\VerifyCsrfToken->handle(Object(Illuminate\Http\Request), Object(Closure))

看起来 Laravel 5 正在自动检查 POST 对 CSFR 令牌的请求(跨站点请求伪造)。这将是您在基于 UI 的浏览器中在表单上创建的内容,以帮助防止跨站点请求伪造。

由于 A CSFR 令牌对于 API 没有意义,您需要删除此中间件。 Laravel 5 应用程序的默认设置似乎为您提供了内核 class 文件

#File: app/Http/Kernel.php
<?php namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel {

    /**
     * The application's global HTTP middleware stack.
     *
     * @var array
     */
    protected $middleware = [
        'Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode',
        'Illuminate\Cookie\Middleware\EncryptCookies',
        'Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse',
        'Illuminate\Session\Middleware\StartSession',
        'Illuminate\View\Middleware\ShareErrorsFromSession',
        'App\Http\Middleware\VerifyCsrfToken',

    ];

    /**
     * The application's route middleware.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => 'App\Http\Middleware\Authenticate',
        'auth.basic' => 'Illuminate\Auth\Middleware\AuthenticateWithBasicAuth',
        'guest' => 'App\Http\Middleware\RedirectIfAuthenticated',
    ];

}    

对于您的 API 唯一项目,您可能只需删除

'App\Http\Middleware\VerifyCsrfToken',

来自 $middlewares 属性 的条目,您就可以开始了。如果您打算使用浏览器 UI 创建一个应用程序,您需要保留此中间件,然后阅读 on Middlewares 以了解如何在特定请求中使用它。 (简短版本?中间件是一种处理每个 HTTP 请求时发生但不影响实际应用程序代码的方法)

@Alan Storm:你对正在发生的事情完全正确。我在 Laracasts 上发布了同样的问题,你所描述的问题也反映在那里。但是,为了在信用到期时给予信用,我收到了一个更优雅的问题解决方案。所以,我在这里重新发布它,以便其他人遇到同样的问题时也能看到它。来自 Laracasts 的 JarekTkaczyk:

The thing is, that Laravel 5 runs VerifyCsrfToken middleware for each request, no exceptions. It verifies that the request is either read (GET, HEAD, OPTIONS) or has valid token.

In your case there is no valid token, so you get TokenMismatchException. Now, to get rid of the problem you can disable this verification for testing environment.

Simply adjust app/Http/Middleware/VerifyCsrfToken.php to this:

public function handle($request, Closure $next)
{
    if ('testing' !== app()->environment())
    {
        return parent::handle($request, $next);
    }

    return $next($request);
}

Now, happy testing!

我想为目前这里的两个提供替代解决方案。这两种解决方案都规避 CSRF 中间件(通过禁用它或修改其背后的代码)。我认为对于您想要 CSRF 保护并想要测试的合法案例,修改测试比修改代码以适应非代表性测试更好。

我也认为有一个类似这样的代码块

if ('testing' !== app()->environment()) {
   // Path taken only when not testing
} 

与@beznez 自己的 一样,您代码中的任何地方都在要求潜在的错误。测试代码应尽可能采用与 anyone/anything else 与代码交互相同的路径。

在请求中实际包含 CSRF 令牌并不难,就像它会出现在正常的浏览器请求中一样。唯一的技巧是您必须启动一个会话,然后将 _token 键添加到您的请求中,并使用 csrf_token() 返回的值(就像在表单中一样)。所以你修改后的测试代码是:

public function testPostMessagesResponse()
{
    Session::start(); // Start a session        

    $response = $this->call(
        'POST', 'Messages', array("key" => "value", "_token" => csrf_token()));

    $this->assertEquals(200, $response->getStatusCode());
}

此博客 post.

对此进行了详细说明

与其禁用 VerifyCsrfToken 中间件,不如在不禁用任何东西的情况下模拟真实的用户会话会更好。

你只需要在你的测试中开始一个新的Session然后在测试csrf保护路由时通过csrfToken

use Session;

...

public function testPostMessagesResponse()
{
    Session::start();
    $params = [
        'key'    => 'value',
        '_token' => csrf_token()
    ];

    $response = $this->call('POST', 'Messages', $params);

    $response->assertSuccessful();
}