PHPUnit: create a ResultPrinter for output in the browser

Posted on by Matthias Noback

This article should be considered an example of creating a result printer for PHPUnit tests. Nevertheless, the use case described here is totally invalid in my opinion.

PHPUnit 3.6 allows us to create our own so-called ResultPrinters. Using such a printer is quite necessary in the case of running your unit tests from within the browser (see my previous post), since we don't print to a console, but to a screen. You can make this all as nice as you like, but here is the basic version of it.

Create the HtmlResultPrinter

First create the file containing your (for example) HtmlResultPrinter, for example in /src/Acme/DemoBundle/PHPUnit/HtmlResultPrinter.

namespace Acme\DemoBundle\PHPUnit;

class HtmlResultPrinter extends PHPUnit_TextUI_ResultPrinter
{
    public function __construct($out = NULL, $verbose = FALSE, $colors = FALSE, $debug = FALSE)
    {
        ob_start(); // start output buffering, so we can send the output to the browser in chunks

        $this->autoFlush = true;

        parent::__construct($out, $verbose, false, $debug);
    }
}

Because normally browsers wait until the complete response was received from the server, before they start to render anything, we start output buffering. The we set autoFlush to true, which means that as soon as something is written using the ResultPrinter's write() method, the output should be flushed to the browser.

As you can see, we also override the $colors parameter of the call to parent::__construct(), so no colors will be rendered (since this will give a strange result in HTML).

Replace write()

While trying to accomplish this, I found out that the write() method needed to be replaced by a slightly different one:

public function write($buffer)
{
    $buffer = nl2br($buffer);

    $buffer = str_pad($buffer, 1024)."\n"; // pad the string, otherwise the browser will do nothing with the flushed output

    if ($this->out) {
        fwrite($this->out, $buffer);

        if ($this->autoFlush) {
            $this->incrementalFlush();
        }
    }
    else {
        print $buffer;

        if ($this->autoFlush) {
            $this->incrementalFlush();
        }
    }
}

First of all: I let this method replace any new line characters by HTML new lines (). Second, the output should reach some critical mass, before it gets processed by a browser; therefore I add padding to every outputted string. Without it, the browser still waits with rendering the content until everything is received.

Finally; I also removed the call to htmlspecialchars() from the original write() method. This will prevent double escaping of HTML characters (like "<" or ">"). Note that if you override any other methods of the ResultPrinter, it is now your own responsibility to take care of output escaping.

Replace incrementalFlush()

Then, upon autoFlush, the method incrementalFlush() gets called. But nothing gets flushed really, since we are using output buffering. So right before flush() gets called (and the output is sent to the browser), the output buffer needs to be flushed also. The ResultPrinter's incrementalFlush() method should thus look like this:

    public function incrementalFlush()
    {
        if ($this->out) {
            fflush($this->out);
        } else {
            ob_flush(); // flush the buffered output
            flush();
        }
    }

With the HtmlResultPrinter in place, we can now modify the action as you can find it in my previous post:

    public function runTestsAction($filterClass = null)
    {
        require_once 'PHPUnit/Autoload.php';

        // this will force the printer class to be autoloaded by Symfony, before PHPUnit tries to (and does not) find it
        $printerClass = 'Acme\DemoBundle\PHPUnit\HtmlResultPrinter';
        if (!class_exists($printerClass)) {
            throw new \RuntimeException('Printer class not found');
        }

        set_time_limit(0);

        $kernel_dir = $this->container->getParameter('kernel.root_dir');

        chdir($kernel_dir); // so PHPUnit thinks it is in the /app directory

        $argv = array(
            '--printer',
            $printerClass,
        );

        if ($filterClass) {
            $argv[] = '--filter';
            $argv[] = $filterClass;
        }

        $_SERVER['argv'] = $argv;

        \PHPUnit_TextUI_Command::main(true); // true means: exit
    }

First, we need to make sure the HtmlResultPrinter class is loaded (otherwise PHPUnit will try to do so and fail). Then, we have to add the --printer command line option to tell PHPUnit which ResultPrinter it should use.

Now we can now decorate the standard TextUI_ResultPrinter methods with our own methods for enhancing the rendered results. For example, wrap each defect in a <div> with a class "defect":

    protected function printDefect(\PHPUnit_Framework_TestFailure $defect, $count)
    {
        $this->write('<div class="defect">');
        parent::printDefect($defect, $count);
        $this->write('</div>');
    }

As you can see, once you have created your HtmlResultPrinter, the possibilities are endless; add some styling to make things prettier, or show a nice image when no errors or failures have occurred...

PHP Testing PHPUnit
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Chris Jones

Great post! Keep writing about Symfony2, your blog has been very helpful. I took this code one step further as I found that some of my testing environments were running PHPUnit 3.5. Rather than having this route fail, I added some simple version checks.

Code posted here: https://gist.github.com/157...

Matthias Noback

Thanks, good to hear. If you have any important additions to the code above, please let me know.