I recently added the Laravel-Localization package (with Laravel 5.2) to a project I’m working on. Initial setup went smoothly, and I was impressed with how well things worked essentially out of the box.
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.
1 2 3 4 |
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:
1 2 3 4 5 |
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.
Alex
Hey, great post! I’ve been pulling my hair out trying to determine the problem for a while now. So in the end, did you find any alternative solutions? Insofar as I can see, testing is practically impossible without intervention into your codebase each time or using Laravel Dusk instead, which obviously takes substantially more time to execute. Any other ways?
Alex
Btw Option 1 is not a tenable solution, as it might lead to subtle bugs. For example, referencing the $errors variable in your views requires that your ShareErrorsFromSession middleware is active; if it’s off, it might take you quite some time to figure out what went wrong. Same for any other middleware on which your app depends implicitly.
munderwood
In the end I switched to the practice of manually writing a test for each English route, with the idea that any accidental (or simply poorly thought through) changes to the routes file would end up causing a test to fail, instead of having the tests automatically change themselves to match, and then still passing. My goal was to set the project up from the start to be easily translatable later, but for the foreseeable future it’s only going to exist in English anyway, so I had to move on to other parts. I haven’t really thought much more about this issue since then, but I’d certainly be interested in knowing about it if you came up with a better option for the automated approach!
As for your point about the first option, I agree that disabling all middleware during testing could lead to a slew of issues. But I just meant that you could choose not to use the LaravelLocalizationRedirectFilter middleware in your application. This would lead to having the routes
/about
and/en/about
both work and both show the same thing, instead of having/about
issue a redirect to/en/about
. But I don’t think it would cause any of the other problems you mention, would it?Alex
So I guess you went with setting ‘hideDefaultLocaleInURL’ to true then, eh? This way the routes for your default locale (‘en’) become non-localized, and you can at least test those. I did precisely that and was able to manipulate app locale with some configuration in phpunit.xml. However, the old problem remains, because there’s no way to test localized routes with any prefix other than ‘en’ — you either get 302 or 404. I did notice though that mcamara had a few tests in his package, but I found them difficult to understand.
Brad
I’m running into the same error, unfortunately. I definitely felt that your first option was sub-optimal and tried the second one. Well, it worked for the English language version of my site, but did not work for anything other than the landing page.
Now that we’re a few months down the road, were you able to solve this in the end?
Thanks for your post :)
Brad
I figured out what the problem was. I had multiple .htaccess files on my shared server. Apparently they were interfering with each other.
I have now cleared out my files and will start again tomorrow. I think it should work then :)
munderwood
Glad you got things (hopefully) sorted!
As I mentioned above, in the end I abandoned the idea of automatically verifying only the routes in the routes file, and instead coded a test for each route so that tests would start failing if breaking changes were made to the routes file…