A great many of the tests use a pattern of getting a response from an endpoint, running $data = $response->decodeResponseJson()
, and then making assertions against the resulting PHP array in $data
. Unfortunately, in Laravel 8 the return type is now \Illuminate\Testing\AssertableJsonString
, and I kept running into errors such as:
Argument #2 of PHPUnit\Framework\Assert::assertArrayHasKey() must be an array or ArrayAccess
I made various attempts to convert the output of decodeResponseJson
into an array (and being confused as to why it didn’t work, since AssertableJsonString implements ArrayAccess
), but in the end the solution was simple.
Instead of refactoring every existing test to use Laravel’s new JSON-based assertions, I merely had to change each call to decodeResponseJson
to just json
. That is, switching to
$data = $response->json();
was all it took.
]]>>>> $faker = Faker\Factory::create();
]]>
Then I tried to test my routes.
I had previously made a test case that ran through all of my public routes and asserted that each one returned HTTP status code 200 OK. Suddenly this was failing on every route I tried, saying that 404 Not Found was being returned instead.
There was 1 failure: 1) AllRoutesOkTest::testGetRoutes A request to [http://localhost/en] failed. Received status code [404].
The route worked in the browser. It returned the correct result using curl
on the vagrant box. But it refused to work in the test. I had the application’s default locale set to ‘en’, and every indication was that it worked perfectly everywhere except in the tests. I pared the test right down:
public function testMinimalExample() { $this->visit('/') ->assertResponseOk(); }
The error was the same. The request was correctly updated from /
to /en
based on the default locale, but apparently Laravel insisted on returning 404 to the test. I would have understood if the status code were 301 or 302, since I’m using the LaravelLocalizationRedirectFilter middleware and expect there to be a redirect. But why would the route simply disappear?
To make a long debugging session short, it turns out that while LaravelLocalizationRedirectFilter middleware correctly leads any browser to make a second request for the new URL, the testing framework tries to follow redirects within the routes available to the same initial request object.
Standard operation of Laravel-Localization is to set up a route group with a prefix value of LaravelLocalization::setLocale()
. When the browser visits the homepage initially, this function returns null so Route::get('/', ...)
is matched, the middleware is invoked, and the browser is redirected to /en
. The browser then invokes a second request, this time to /en
as directed, which means setLocale()
is called again, this time returning 'en'
. So Route::get('/', ...)
is now prefixed by 'en'
, the requested route is matched, and everything works correctly.
In the test however, there is only ever a single request object, in which the prefix defined by setLocale()
is simply ''
. This matches the first time, the server runs the middleware and sends a redirect, but instead of making a second request, the testing framework tries to follow the redirect by resolving the new route against the RouteCollection
in the original Request
object. Since it was created with a prefix of ''
, any route prefixed by 'en'
won’t exist.
So, how do we solve this? I’ve found two interim solutions so far, though I plan to keep looking another day.
Option 1 is to disable the LaravelLocalizationRedirectFilter middleware. This means that requests to un-locale-prefixed routes will work just fine, but won’t be redirected to their prefixed versions, resulting in two different URLs for each resource in the default language.
Option 2 is to set 'hideDefaultLocaleInURL' => true
in the laravellocalization.php
config file. This results in the redirection of all URLs prefixed with the default locale to unprefixed ones—/en
becomes /
, /en/users
becomes /users
, and so on. This is perhaps preferable to Option 1 from an SEO standpoint, but comes with the caveat that the Accept-Language header of any user’s browser will now be ignored.
Likely some other options exist that involve changing the requested routes in the test cases instead.
]]>One interesting thing to note is that using curly braces instead of round brackets in your blade templates can lead to not-obviously-related error messages. For instance, my first attempt at a template began with
// Incorrect way to extend a layout: // Note the use of {} instead of () @extends{'layout.php'} // DO NOT DO THIS!
instead of the correct
// Correct way to extend a template @extends('layout.app')
While obviously this is a simple typo that is entirely my own fault, the resulting error message,
FatalErrorException in fba8cddd62bd85dee11955629f018a61 line 24:
syntax error, unexpected ‘,’
was not particularly enlightening, and I spent rather longer than I would have liked before noticing my error.
The tutorial seems to leave out two important lines when it comes to authorising the deletion of tasks via policy. Specifically, when the tutorial gets to the point of adding the new policy to app/Providers/AuthServiceProvider.php ,
protected $policies = [ Task::class => TaskPolicy::class, ];
it fails to mention that this won’t work until the lines
// app/Providers/AuthServiceProvider.php // ... use App\Task; use App\Policies\TaskPolicy; // ...
are also added to the file.
Finally, if you create your own files from scratch for the tutorial instead of cloning their repo, then registering and logging in will appear to result in an error,
NotFoundHttpException in RouteCollection.php line 161:
This can be surprising at first, and might make you think there actually is an error in your code, but of course it is essentially just a 404 Not Found error—by default you are redirected to /home , which hasn’t been created. One option would of course be to create home view. Another, which is what the Laravel repo does, is to override the redirection destination in app/Http/Controllers/Auth/AuthController.php and point it at /tasks instead.
To do so, simply set the $redirectTo property to the desired path, for example
// app/Http/Controllers/Auth/AuthController.php class AuthController extends Controller { // ... use AuthenticatesAndRegistersUsers, ThrottlesLogins; protected $redirectTo = '/tasks'; // ... }
]]>