PHP & Symfony About PHP and Symfony2 development

Test Symfony2 commands using the Process component and asynchronous assertions

Posted on by Matthias Noback

A couple of months ago I wrote about console commands that create a PID file containing the process id (PID) of the PHP process that runs the script. It is very usual to create such a PID file when the process forks itself and thereby creates a daemon which could in theory run forever in the background, with no access to terminal input or output. The process that started the command can take the PID from the PID file and use it to determine whether or not the daemon is still running.

Below you find some sample code of what a daemon console command would look like. It forks itself using pcntl_fork(), a fascinating function which has different return values at the same time (but in different processes!).

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class DaemonCommand extends Command
{
    protected function configure()
    {
        $this->setName('daemon');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $pid = pcntl_fork();

        if ($pid === -1) {
            throw new \RuntimeException('Could not fork the process');
        } elseif ($pid > 0) {
            // we are the parent process
            $output->writeln('Daemon created with process ID ' . $pid);
        } else {
            file_put_contents(getcwd() . '/daemon.pid', posix_getpid());
            // do something in the background
            sleep(100);
        }
    }
}

Usually you would test a console command using the ConsoleTester class as described in the official Symfony documentation. But we can not use it in this case. We need to isolate the process in order for pcntl_fork() to work correctly, otherwise the PHPUnit test runner itself will be forked too, which will have the effect of running all the following unit tests twice (which can have very strange effects on the test results).

So first we need to start the daemon command in its own process. We can use the Symfony Process Component for this.


use Symfony\Component\Process\Process; class DaemonCommandTest extends \PHPUnit_Framework_TestCase { /** * @test */ public function it_creates_a_pid_file() { $process = new Process('php app/console daemon'); $process->start(); } }

However, since our console commands crosses the boundaries of one process, this will not suffice. As soon as the daemon child process has been created, it has a life on its own and you can not be sure if and when the daemon has executed its tasks, like creating a PID file. Simply calling assertTrue() like this

public function it_creates_a_pid_file()
{
    $process = new Process('php app/console daemon');
    $process->start();

    $this->assertTrue(file_exists('daemon.pid'));
}

would be nice, but turns out to be unreliable. Tests like these are called "flickering tests", since they will sometimes fail, sometimes succeed, in a very unpredictable manner. So before we start making assertions about the expected behavior of the daemon we need to wait a reasonable amount of time for the child process to get up and running:

public function it_creates_a_pid_file()
{
    $process = new Process('php app/console daemon');
    $process->start();

    // 5 seconds should be enough to start the daemon, right?
    sleep(5);

    $this->assertTrue(file_exists('daemon.pid'));
}

Better safe than sorry: we wait 5 seconds and make the assertion. This is of course really bad for your test suite. Where the other 1000 unit tests all run in one second, this one test takes at least 5 seconds! Even worse, 5 seconds may not even be enough when, for instance, the daemon somehow triggers the Symfony cache to be rebuilt, which takes much longer than that on most machines. Then again, if the cache does not need to be rebuilt, 5 seconds may be way too much, and 500 milliseconds would be more appropriate.

Polling

Instead of putting the process into a long sleep we need to check regularly if the daemon.pid file has been created, make an assertion and leave the test method as soon as possible. In other words, we need a polling mechanism and a probe. The probe inspects the current system's condition and lets the polling mechanism know if it is happy about that condition. In case the desired condition is never reached, the polling mechanism keeps track of time and gives up after a number of seconds (I read about this first in Growing Object-Oriented Software, Guided by Tests, an excellent book that taught me many new and interesting things about TDD).

In the case described above, we could implement such a polling/probing/timeout mechanism with a simple loop:

$keepTrying = true;
$startTime = time();
while ($keepTrying) {
    if (time() - $startTime > 5) {
        $this->fail('We waited for 5 seconds but the PID file was not created');
    }

    if (file_exists('daemon.pid')) {
        $keepTrying = false;
    }
}

Well, I don't like this type of error-prone code in my test methods so I decided to abstract some things into a fully tested library for PHPUnit, which is called PHPUnit Asynchronicity. You can install it in your project using Composer (the package is called matthiasnoback/phpunit-asynchronicity). It allows you to use one simple assertion to accomplish the same thing as described above:

use Matthias\PhpUnitAsynchronicity\Eventually;

class DaemonCommandTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @test
     */
    public function it_creates_a_pid_file()
    {
        $process = new Process('php app/console daemon');
        $process->start();

        $this->assertThat(
            function () {
                return file_exists('daemon.pid');
            },
            new Eventually()
        );
    }
}

You could abstract this a bit further, which would make it better readable:

class DaemonCommandTest extends \PHPUnit_Framework_TestCase
{
    public function it_creates_a_pid_file()
    {
        ...

        $this->assertEventually(
            function () {
                return file_exists('daemon.pid');
            }
        );
    }

    private function assertEventually($probe)
    {
        $this->assertThat($probe, new Eventually());
    }
}

The Eventually class is a PHPUnit constraint that accepts two arguments: a timeout in milliseconds (so 5000 milliseconds would mean a timeout of 5 seconds) and a wait time in milliseconds. After each wait time the probe will again be asked to examine the current state of the system.

By the way, it's also possible to create a probe that is not a closure, but a stand-alone class:

use Matthias\Polling\ProbeInterface;

class PidFileExists implements ProbeInterface
{
    public function isSatisfied()
    {
        return file_exists('daemon.pid');
    }
}

Then it reads even better:

class DaemonCommandTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @test
     */
    public function it_creates_a_pid_file()
    {
        ...

        $this->assertEventually(new PidFileExists());
    }
}

Categories: PHP Symfony2 Testing

Tags: asynchronicity console PHPUnit PID

Comments: Comments