Testing framework-backed APIs with PHP’s internal web server

Recently I had to prepare functional tests for a set of (RESTish) APIs in a Silex-backed application. The problem is that I couldn’t test POST, PUT and DELETE methods on the development environment or (worse) on the staging server.

So I came up with the idea of using the PHP integrated webserver, started from the PHPUnit bootstrap, and a couple of rules in the app bootstrap to redirect all storage calls to a temporary database.

Directory structure

The project structure is fairly basic, with root-level app (application’s files: models, controllers, etc.), web (public files: the front-controller, css, javascript, etc.), vendor (the composer’s folder) and tests (the PHPUnit tests directory):

project root/
 |- app/
 |   |- Controller/
 |   |- Model/
 |   ¦- ...
 |- vendor/
 |   ¦- ...
 |- tests/
 |   |- bootstrap.php
 |   |- htaccess.php
 |   ¦- ...
 |- web/
     |- index.php
     |- .htaccess
     |- css/
     ¦- ...

PHPUnit bootstrap

Let’s start with the PHPUnit’s bootstrap.php file. I found someone who did it before I did, so I copied his solution. :) I just changed it a bit to simulate the htaccess (see next section).

<?php

// Require composer's autoload
$loader = require __DIR__.'/../vendor/autoload.php';

// Define server parameters
define('WEB_SERVER_HOST', '127.0.0.1');
define('WEB_SERVER_PORT', 8765);

// Command that starts the built-in web server
// See http://tech.vg.no/2013/07/19/using-phps-built-in-web-server-in-your-test-suites/
$command = sprintf(
    'php -S %s:%d -t %s %s >%s 2>&1 & echo $!',
    WEB_SERVER_HOST,
    WEB_SERVER_PORT,
    realpath(__DIR__ . '/../html/'),
    realpath(__DIR__ . '/htaccess.php'),
    '/tmp/myapp-testing/webserver.log'
);

// Execute the command and store the process ID
$output = array();
exec($command, $output);
$pid = (int) $output[0];

echo sprintf(
        '%s - Web server started on %s:%d with PID %d',
        date('r'),
        WEB_SERVER_HOST,
        WEB_SERVER_PORT,
        $pid
    ) . PHP_EOL;

// Kill the web server when the process ends
register_shutdown_function(function() use ($pid) {
    echo sprintf('%s - Killing process with ID %d', date('r'), $pid) . PHP_EOL;
    exec('kill ' . $pid);
});

As you can see, it’s launching the webserver with a custom front-controller (htaccess.php) that will simulate the Apache’s .htaccess, and a log file in /tmp. Naturally you will need to change the paths to your needs.

htaccess.php, the .htaccess simulator

As I am rewriting URLs (via Silex’s router), I need URL rewriting in Apache/nginx, but PHP’s internal webserver does not support that, so we will need to simulate them via a front-front-controller.

There are some examples online, but most of them (including the PHP manual’s one) just return false if the file exists. As we are not working in the public directory, we cannot use this shortcut, and need to serve the files with the correct Content-type instead. Here I just included css and js files. As I needed it to test API access, I would have not needed it at all, but I included it for possible future uses.

<?php

define('APP_ENV', 'TESTING');

$fullPath = __DIR__ . '/../web' .$_SERVER['REQUEST_URI'];

if ( file_exists($fullPath) ) {
    $info = pathinfo($fullPath);
    switch ($info['extension']) {
        case 'css':
            $mime = 'text/css';
        break;

        case 'js':
            $mime = 'application/javascript';
        break;

        default:
            $mime = mime_content_type($fullPath);
        break;
    }

    header('Content-Type: ' .$mime  );
    print file_get_contents($fullPath);
    exit;

} else {
    include __DIR__ . '/../web/index.php';
}

Two things to note here:

  1. The define() at the beginning. It will be used in the (main) index.php to know we are running in a testing environment, so to configura paths and DB connections accordingly, and not overwrite the development ones.

  2. The include in the last line, that will include the main index.php, our application’s “front-controller”.

  • KevMTL

    I’m also running an internal web server for my tests.
    How do you handle code coverage on the external call ?