Проблемы с тестированием HTTP-глаголов с помощью PHPUnit в Laravel

Прежде чем начать, я хотел бы сказать, что я уже посмотрел на Не удалось подтвердить статус HTTP 200, а не 500 так же как Не удалось установить код состояния HTTP 200, а не 500. Не позволяйте именам обмануть вас; это 2 разных поста. Оба этих поста старше 9 месяцев и не имеют хороших ответов.

У меня проблемы с тестированием маршрутов. Итак, я сделал новый Laravel 5 проект на моем Mac Mini работает OS X Yosemite 10.10.2, Я делаю API. Итак, я начал делать бэкэнд-часть API первым. Я решил, что хочу использовать PHPUnit реализовать TDD для этого проекта. Моя версия PHPUnit является 4.0.20, я бегу PHP 5.5.14 и я использую сервер в комплекте с Laravel в artisan инструмент командной строки.

Теперь я начал с того, что должен был просто подключиться к серверу. Итак, мой первый тест выглядит так:

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.

В моем файле rout.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

Это возвращает Laravel «Ой!» стр. Так что действительно немного ошибка сервера, но я не уверен, что искать. Поток приложений выглядит хорошо, но он, очевидно, не работает. Вот трассировка стека из моего журнала 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), но мой пост (Route::post) все еще получает ту же самую ошибку 500. Функция, охватывающая строку 2375 в … / compiled.php (из исключения, сгенерированного в верхней части трассировки стека), генерируется 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());
}

Я получаю то же сообщение об ошибке. Я чувствую, что упускаю что-то простое, но я не могу понять это. Любая помощь будет принята с благодарностью.

2

Решение

Вы можете увидеть свою проблему в первой функции стека вызовов

#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 (подделка межсайтового запроса). Это будет то, что вы создадите в своей форме в браузерном интерфейсе, чтобы предотвратить подделку межсайтовых запросов.

Так как токен CSFR не имеет смысла для API, вам нужно удалить это промежуточное ПО. Кажется, что настройка по умолчанию для приложения Laravel 5 дает вам файл класса Kernal

#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 собственности, и вы будете хорошо идти. Если вы собираетесь создать приложение с пользовательским интерфейсом браузера, вам нужно сохранить это промежуточное ПО, а затем прочитать на Middlewares чтобы увидеть, как вы можете использовать его по конкретным запросам. (Короткая версия? Промежуточные программы — это способ выполнять действия, которые происходят с каждым HTTP-запросом, но не влияют на реальный код приложения)

5

Другие решения

@Alan Storm: вы абсолютно правы в происходящем. Я разместил этот же вопрос на Laracasts, и проблема, которую вы описываете, отражена там. Тем не менее, чтобы отдать должное, когда это необходимо, я получил более элегантное решение проблемы. Итак, я публикую это здесь, чтобы у других была такая же проблема. От JarekTkaczyk на Laracasts:

Дело в том, что Laravel 5 запускает промежуточное ПО VerifyCsrfToken для каждого запроса, без исключений. Он проверяет, что запрос либо прочитан (GET, HEAD, OPTIONS), либо имеет действительный токен.

В вашем случае нет действительного токена, поэтому вы получаете TokenMismatchException. Теперь, чтобы избавиться от проблемы, вы можете отключить эту проверку для тестирования среды.

Просто настройте app / Http / Middleware / VerifyCsrfToken.php на это:

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

return $next($request);
}

Теперь счастливого тестирования!

6

Я хотел бы предложить альтернативное решение для двух в настоящее время здесь. Оба решения введение в заблуждение промежуточное программное обеспечение CSRF на месте (либо отключив его, либо изменив код за ним). Я думаю, что для законных случаев, когда вы хотите защитить CSRF и хотите провести тестирование, лучше модифицировать тест, чем модифицировать код для приспособления к нерепрезентативному тесту.

Я также думаю, что имея кодовый блок, похожий на этот

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

как у самого @ beznez ответ, где-нибудь в вашем коде просят скрытых ошибок в будущем. Насколько это возможно, тестовый код должен идти по тому же пути, что и любой другой пользователь, взаимодействующий с кодом.

Не так сложно на самом деле иметь токен 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());
}

Это подробно описано в этом блоге сообщение.

4

Вместо того, чтобы отключить VerifyCsrfToken Промежуточное программное обеспечение, было бы намного лучше просто имитировать сеанс реального пользователя, не отключая ничего.

Вам просто нужно начать новый Session внутри вашего теста, а затем передать csrfToken при тестировании csrf защищенных маршрутов.

use Session;

...

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

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

$response->assertSuccessful();
}
0
По вопросам рекламы [email protected]