Laravel has an incredible amount of testing utilities built right in. Anybody that knows me (or that has read my past articles) knows that I believe one of the best parts of Laravel is how much work has been put into testing integrations. Regardless of your opinions about Laravel, check out their testing documentation (make sure to look at the side navigation for the other links), it really is marvelous.
The natural place to start with testing is with HTTP tests, and Laravel calls these “Feature Tests”. For the uninitiated, Feature Tests make a mock HTTP request to your application and allow you to make assertions against the response.
Note: if you’re already familiar with this issue, and only interested in the path to troubleshooting it, feel free to skip ahead.
The Situation
Every now and then I find myself helping somebody who can’t seem to get Feature Tests working outside of a basic GET request. While that’s great, it’s probably pretty important to make POST/PUT/DELETE requests as well, right?
Just for clarification, let’s write a simple HTTP test that makes a POST request:
// routes/web.php:
Route::post('/', function () {
return 'foo';
});
// tests/Feature/ExampleTest.php:
public function testFoo()
{
$response = $this->post('/');
$response->assertStatus(200);
}
If you created a brand new Laravel application this test would absolutely pass; unfortunately, people often start writing tests at different points through an application’s development cycle, and sometimes this test fails. In this particular case, I’m going to talk about this test specific failure:
1) Tests\Feature\ExampleTest::testFoo
Expected status code 200 but received 419.
Failed asserting that false is true.
Okay, I’ll admit that this is fairly cryptic. What does HTTP status 419 mean? It’s not even in the Wikipedia article! Fortunately for us, Laravel 5.5 introduced a really cool utility method on the default TestCase
object: withoutExceptionHandling()
. All we have to do is add $this->withoutExceptionHandling()
at the top of our testFoo()
method and any exception that gets handled by Laravel’s Exception Handler will get re-thrown and we’ll see it in our console. If you’d like this behavior in Laravel 5.4 and below, you can see Adam Wathan’s blog article which inspired this feature.
After adding $this->withoutExceptionHandling()
, we now see this:
1) Tests\Feature\ExampleTest::testFoo
Illuminate\Session\TokenMismatchException:
(omitting the long stack trace)
Sometimes, people go ahead and add a token key and value to their request data to get around this error. Sure, this is a short term solution, but who wants to write that for every request? Lucky for us, this is easy enough to troubleshoot!
Troubleshooting
All of the examples will be with Laravel 5.6, as that’s the current version at the time of writing this.
If we take a look at HTTP Kernel’s web group of middleware, we’ll see \App\Http\Middleware\VerifyCsrfToken::class
, which is probably a good place to start! Most projects don’t modify this class, so it will look something like this:
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
//
];
}
If you upgraded from an earlier version of Laravel, it might be slightly different, but the concept will remain the same. Notice how this class is mostly empty? We’ll need to defer to the parent class to see the behavior. Since this class is 167 lines, I’m only going to show the relevant part:
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*
* @throws \Illuminate\Session\TokenMismatchException
*/
public function handle($request, Closure $next)
{
if (
$this->isReading($request) ||
$this->runningUnitTests() ||
$this->inExceptArray($request) ||
$this->tokensMatch($request)
) {
return $this->addCookieToResponse($request, $next($request));
}
throw new TokenMismatchException;
}
If you aren’t familiar with Laravel Middleware, go ahead and give it a read as it’s not too hard to understand. For now, just know that all middleware will call the handle()
method. Honestly, getting this far is probably the scariest part. It’s pretty clear here that $this->runningUnitTests()
must be failing. But why?! We are definitely running this from inside a test!
Okay, let’s go down the rabbit hole! Let’s see what runningUnitTests()
looks like:
protected function runningUnitTests()
{
return $this->app->runningInConsole() && $this->app->runningUnitTests();
}
Okay, that’s pretty simple, right? From here, it’s as simple as firing up Xdebug and adding a breakpoint or throwing in a dd()
to check the output of $this->app->runningInConsole()
and $this->app->runningUnitTests()
. So another step down the rabbit hole we go:
/**
* Determine if the application is running in the console.
*
* @return bool
*/
public function runningInConsole()
{
return php_sapi_name() === 'cli' || php_sapi_name() === 'phpdbg';
}
/**
* Determine if the application is running unit tests.
*
* [@return](http://twitter.com/return) bool
*/
public function runningUnitTests()
{
return $this['env'] === 'testing';
}
While it’s a pretty good bet that runningInConsole()
will return true, it doesn’t hurt to verify. For the purpose of this article, I’m going to assume it does return true and that runningUnitTests()
is failing. Now, the obvious place to look is config/app.php
which has a line specifically for 'env' => env('APP_ENV', 'production')
. It’s very likely that your .env
file has APP_ENV=local
. Well that’s the problem, that says local instead of testing! Well, not so fast. A default installation of Laravel has this line in phpunit.xml
: <env name="APP_ENV" value="testing"/>
. It’s certainly worth verifying that line is present and the value is still set to testing.
The Likely Suspects
So what if it isn’t that easy? Everything in phpunit.xml
looks fine, and out of extreme paranoia we changed .env
to have APP_ENV=testing
(just to clarify, you don’t actually need to change that, and probably shouldn’t), but we still see that $this['env']
is not testing
. At this point, you will want to start looking at setUp()
methods, both in the test class you are inside of, and tests/TestCase.php
.
You’ll want to be on the lookout for any of the following:
config(['app.env' => 'not-testing'])
config()->set('app.env', 'not-testing')
Config::set('app.env', 'not-testing')
putenv('APP_ENV=not-testing')
App::detectEnvironment()
App::detectEnvironment()
Note: detectEnvironment()
will have a callback passed to it. Unless your project was upgraded to Laravel 5 from an earlier version, you likely won’t have to worry about this.
While you scour through the setUp()
methods, you might want to make sure you always call parent::setUp()
while you are inside of it to prevent other negative side effects. It’s also worth looking at the createApplication()
function as well (which was moved to a trait in Laravel 5.4 and above).
If you still come up empty handed, it’s probably worth doing a search in the entire tests
directory for the value that you see in $this['env']
, and/or checking any sort of Vagrant/Homestad/Docker configuration files you might have.
While something changed in a test file somewhere is the likely culprit, it’s worth nothing that somebody could be particularly evil and change the environment in the application’s/request lifecycle before the middleware. If somebody were to have fun inside bootstrap/app.php
or even a middleware that is loaded before VerifyCsrfToken
. The HTTP kernel discussed earlier has a middleware priority instance variable that can be overridden.
This article ended up being a bit longer than I initially expected, but hopefully this gives you more confidence to look through Laravel’s source code on your own, it really isn’t that scary!
Top comments (0)