A Quick Fix To Codeception’s PhpBrowser Throwing 404 Errors Upon Testing A Laravel App On A VirtualHost

Warning: To a non tech-savvy reader, this may probably be one of the most abstruse post titles ever. Sorry about that. Move along if you will, I promise I won’t hold it against you.

Having found a dirt-simple solution to an obscure problem I’ve been having for hours, I felt that I should publish the fix so that it may help others. It’s one of those things that take literally hundreds of lines of code to diagnose and just over 10 characters to solve for good.

Ready? Let’s get down to it! (TL;DR)

The Problem

So I have been working on Laravel for a few days now, and it is a real delight. Every one of its components is designed in a clean, beautiful way and whatever construed goal you’re doing to achieve, whatever methodology you’re using, the framework almost never goes against your will. As part of my journey into building a solid Web application, I wanted to use BDD and especially automated acceptance testing with the Codeception framework. For the few who were brave enough to actually read on without even using Codeception, kudos to you —also, you’re missing out; install it at once. Here’s a sample of code that you can write to test for the login feature of your application:

<?php
$I = new WebGuy($scenario);
$I->wantTo('log into the application as a regular user');
$I->amOnPage('/'); // home page
$I->dontSee('Logout');
$I->see('Login'); // login link implies we're logged out
$I->click('Login');
$I->see('Email');
$I->see('Password');
$I->fillField('email', 'regular@local');
$I->fillField('password', 'regular');
$I->click('input[type="submit"]'); // submit the form
$I->see('Success'); // login notification
$I->see('Logout'); // Logout link implies we're logged in

The syntax is so clear and simple tests can actually be written by people who can’t be bothered to code. It’s refreshing and I love it. Anyway, I was aiming at running this test on my Laravel install but I was getting weird bugs: browsing and analysing the first page was fine, however Codeception’s internal Browsing engine —PhpBrowser— couldn’t browse to any other page. Meaning the tests on the home page would all be good but clicking on a link of submitting the form systematically led to errors such as this one in the console:

Failed asserting that<br />
--> Whoops! There was an error..cf:before, .cf:after {content: " ";display: table;} .cf:after {clear: both;} .cf {*zoom: 1;} body { font: 14px "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif; color: #2B2B2B; background-color: #e7e7e7; padding:0; margin: 0; max-height: 
[Content too long to display. See complete response in '_log' directory]<br />
--> contains "xxxx".

The Cause

After a quick yet painful analysis, I learned that those errors are actually caused by Codeception’s PhpBrowser choking on a 404 error. That’s fine and well, dead pages happen, yet at the same time the application was working wonderfully for me in all major browsers. As an added bonus, the first page tested by the Codeception story was systematically loading properly, be it the homepage, login page or whatever other location. This was extremely confusing: why the hell would a browser work perfectly well on the first page opened with it but fail all subsequent loads? Turns out the bug has to do with the way Codeception, PhpBrowser and Laravel can be intertwined.

See, out-of-the-box, PhpBrowser doesn’t play well with Virtual Hosts. And as local installations of Laravel typically use a VHost pointing to /var/www/xxxx.dev/public so that we can use http://xxxx.dev/ and hit the app, any test involving browsing will fail. In order to make Codeception load the first page without errors, which is great in itself, you need to add the :80 port number to your app url in acceptance.yml:

class_name: WebGuy
modules:
     enabled: [PhpBrowser, WebHelper]
     config:
          PhpBrowser:
               url: 'http://xxxx.dev:80/'

Now your tests can access the first page they try to load and act on it. However, browsing away from the first page will fail because of Laravel’s way of handling URLs. Laravel uses a method borrowed from Symfony to get the root URL from which the URL builder derives any URL generated with the url() helper. There is one thing however that you can’t guess without having read the actual code:

    public function getHttpHost()
    {
        $scheme = $this->getScheme();
        $port   = $this->getPort();

        if (('http' == $scheme && $port == 80) || ('https' == $scheme && $port == 443)) {
            return $this->getHost();
        }

        return $this->getHost().':'.$port;
    }

Yep, that’s right: if the port is 80, all URLs generated by Laravel will lack the port number, making PhpBrowser unhappy! Damn.

The Fix

Oh, well. At least now we know what’s causing all the fuss, the fix is dirt-simple: change the port used by your local server to anything else than 80. I’m using 8081 myself, but feel free to choose anything really. Here’s how it’s done in Apache2’s ports.conf:

Listen 80
Listen 8081

Once you’ve edited apache2/ports.conf, open sites-enabled/xxxx.dev.conf and change the port, then save and restart your server:

$ sudo service apache2 restart

And voilĂ ! I know this is an incredibly long post for a really tiny fix. I tried to detail as much as I could while including as many keywords related to the issue as possible so that hopefully people having the same type of issue as I had will be able to find this page from Google. Thanks for bearing with me!

Leave a Reply