How to Write JavaScript-Style Test Watchers in PHP

Share this article

How to Write JavaScript-Style Test Watchers in PHP

This article was peer reviewed by Younes Rafie. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!


I didn’t start out writing tests for my code. Like many before and since, my “testing” was to write code and refresh the page. “Does it look right?”, I’d ask myself. If I thought so, I’d move on.

In fact, most of the jobs I’ve had have been with companies who don’t much care for other forms of testing. It’s taken many years, and wise words from people like Chris Hartjes, for me to see the value in testing. And I’m still learning what good tests look like.

Vector icon with eye

I recently started working on a few JavaScript projects which had bundled test watchers.

Here’s a great premium video tutorial about test driven NodeJS development!

In the land of JavaScript, it’s not uncommon to preprocess source code. In the land of JavaScript, developers write in syntax not widely supported, and the code is transformed into syntax that is widely supported, usually using a tool called Babel.

In order to reduce the burden of invoking the transformation scripts, boilerplate projects have started to include scripts to automatically watch for file changes; and thereafter invoke these scripts.

These projects I’ve worked on have used a similar approach to re-run unit tests. When I change the JavaScript files, these files are transformed and the unit tests are re-run. This way, I can immediately see if I’ve broken anything.

The code for this tutorial can be found on Github. I’ve tested it with PHP 7.1.

Setting up the Project

Since starting to work on these projects, I’ve started to set a similar thing up for PHPUnit. In fact, the first project I set up the PHPUnit watcher script on was a PHP project that also preprocesses files.

It all started after I added preprocessing scripts to my project:

composer require pre/short-closures

These particular preprocessing scripts allow me to rename PSR-4 autoloaded classes (from path/to/file.phppath/to/file.pre), to opt-in to the functionality they provide. So I added the following to my composer.json file:

"autoload": {
    "psr-4": {
        "App\\": "src"
    }
},
"autoload-dev": {
    "psr-4": {
        "App\\Tests\\": "tests"
    }
}

This is from composer.json

I then added a class to generate functions with the details of the current user session:

namespace App;

use Closure;

class Session
{
    private $user;

    public function __construct(array $user)
    {
        $this->user = $user;
    }

    public function closureWithUser(Closure $closure)
    {
        return () => {
            $closure($this->user);
        };
    }
}

This is from src/Session.pre

To check if this works, I’ve set up a small example script:

require_once __DIR__ . "/vendor/autoload.php";

$session = new App\Session(["id" => 1]);

$closure = ($user) => {
    print "user: " . $user["id"] . PHP_EOL;
};

$closureWithUser = $session->closureWithUser($closure);
$closureWithUser();

This is from example.pre

…And because I want to use the short closures in a non-PSR-4 class, I also need to set up a loader:

require_once __DIR__ . "/vendor/autoload.php";

Pre\Plugin\process(__DIR__ . "/example.pre");

This is from loader.php

This is a lot of code to illustrate a small point. The Session class has a closureWithUser method, which accepts a closure and returns another. When called, this new closure will call the original closure, providing the user session array as an argument.

To run all of this, type into terminal:

php loader.php

As a side-note, the valid PHP syntax that these preprocessors generated is lovely. It looks like this:

$closure = function ($user) {
   print "user: " . $user["id"] . PHP_EOL;
};

…and

public function closureWithUser(Closure $closure)
{
   return [$closure = $closure ?? null, "fn" => function () use (&$closure) {
       $closure($this->user);
   }]["fn"];
}

You probably don’t want to commit both php and pre files to the repo. I’ve added app/**/*.php and examples.php to .gitignore for that reason.

Setting up the Tests

So how do we test this? Let’s start by installing PHPUnit:

composer require --dev phpunit/phpunit

Then, we should create a config file:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit
    backupGlobals="false"
    backupStaticAttributes="false"
    bootstrap="vendor/autoload.php"
    colors="true"
    convertErrorsToExceptions="true"
    convertNoticesToExceptions="true"
    convertWarningsToExceptions="false"
    processIsolation="false"
    stopOnFailure="false"
    syntaxCheck="false"
>
    <testsuites>
        <testsuite>
            <directory suffix="Test.php">tests</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist addUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">src</directory>
        </whitelist>
    </filter>
</phpunit>

This is from phpunit.xml

Were we to run vendor/bin/phpunit, it would work. But we don’t have any tests yet. Let’s make one:

namespace App\Tests;

use App\Session;
use PHPUnit\Framework\TestCase;

class SessionTest extends TestCase
{
    public function testClosureIsDecorated()
    {
        $user = ["id" => 1];
        $session = new Session($user);

        $expected = null;

        $closure = function($user) use (&$expected) {
            $expected = "user: " . $user["id"];
        };

        $closureWithUser = $session
            ->closureWithUser($closure);

        $closureWithUser();

        $this->assertEquals("user: 1", $expected);
    }
}

This is from tests/SessionTest.php

When we run vendor/bin/phpunit, the single test passes. Yay!

What Are We Missing?

So far, so good. We’ve written a tiny bit of code, and a test for that code. We don’t even need to worry about how the preprocessing works (a step up from JavaScript projects).

The troubles begin when we try to check code coverage:

vendor/bin/phpunit --coverage-html coverage

Since we have a test for Session, the coverage will be reported. It’s a simple class, so we already have 100% coverage for it. But if we add another class:

namespace App;

class BlackBox
{
    public function get($key)
    {
        return $GLOBALS[$key];
    }
}

This is from src/BlackBox.pre

What happens when we check the coverage? Still 100%.

This happens because we don’t have any tests which load BlackBox.pre, which means it is never compiled. So, when PHPUnit looks for covered PHP files, it doesn’t see this preprocess-able file.

Building All Files before Testing

Let’s create a new script to build all the Pre files, before trying to run the tests:

require_once __DIR__ . "/../vendor/autoload.php";

function getFileIteratorFromPath($path) {
    return new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($path),
        RecursiveIteratorIterator::SELF_FIRST
    );
}

function deleteFilesBeforeTests($path) {
    foreach (getFileIteratorFromPath($path) as $file) {
        if ($file->getExtension() === "php") {
            unlink($file->getPathname());
        }
    }
}

function compileFilesBeforeTests($path) {
    foreach (getFileIteratorFromPath($path) as $file) {
        if ($file->getExtension() === "pre") {
            $pre = $file->getPathname();
            $php = preg_replace("/pre$/", "php", $pre);

            Pre\Plugin\compile($pre, $php, true, true);

            print ".";
        }
    }
}

print "Building files" . PHP_EOL;

deleteFilesBeforeTests(__DIR__ . "/../src");
compileFilesBeforeTests(__DIR__ . "/../src");

print PHP_EOL;

This is from tests/bootstrap.php

Here we create 3 functions; one for getting a recursive file iterator (from a path), one for deleting the files of this iterator, and one for re-compiling the Pre files.

We need to substitute the current bootstrap file in phpunit.xml:

<phpunit
    bootstrap="tests/bootstrap.php"
    ...
>

This is from phpunit.xml

Now, whenever we run the tests, this script will first clean and rebuild all the Pre files to PHP files. Coverage is correctly reported, and we can be on our merry way…

Except for This Other Thing…

Our codebase is tiny, but it doesn’t need to be. We could try this in a real application, and immediately regret having to rebuild files every time we want to test.

In this project I mentioned, I have 101 Pre files. That’s a lot of preprocessing just to run my (hopefully fast) unit test suite. What we need is a way to watch for changes, and only rebuild the bits that matter. To begin, let’s install a file watcher:

composer require --dev yosymfony/resource-watcher

Then, let’s create a test script:

#!/usr/bin/env php
<?php

require_once __DIR__ . "/../tests/bootstrap.php";

use Symfony\Component\Finder\Finder;
use Yosymfony\ResourceWatcher\ResourceWatcher;
use Yosymfony\ResourceWatcher\ResourceCacheFile;

$finder = new Finder();

$finder->files()
    ->name("*.pre")
    ->in([
        __DIR__ . "/../src",
        __DIR__ . "/../tests",
    ]);

$cache = new ResourceCacheFile(
    __DIR__ . "/.test-changes.php"
);

$watcher = new ResourceWatcher($cache);
$watcher->setFinder($finder);

while (true) {
    $watcher->findChanges();

    if ($watcher->hasChanges()) {
        // ...do some rebuilding
    }

    usleep(100000);
}

This is from scripts/watch-test

The script creates a Symfony finder (to scan our src and tests folders). We define a temporary change file, but it’s not strictly required for what we’re doing. We follow this up with an infinite loop. The ResourceWatcher has a method we can use to see if any files have been created, modified, or deleted.

New, let’s find which files have changed, and rebuild them:

if ($watcher->hasChanges()) {
    $resources = array_merge(
        $watcher->getNewResources(),
        $watcher->getDeletedResources(),
        $watcher->getUpdatedResources()
    );

    foreach ($resources as $resource) {
        $pre = realpath($resource);
        $php = preg_replace("/pre$/", "php", $pre);

        print "Rebuilding {$pre}" . PHP_EOL;

        Pre\Plugin\compile($pre, $php, true, true);
    }

    // ...re-run tests
}

This is from scripts/watch-test

This code is similar to what we did in the bootstrap file, but it is only applied to changed files. When a file changes, we should also re-run the tests:

if (empty(getenv("APP_COVER"))) {
    passthru("APP_REBUILD=0 composer run test");
} else {
    passthru("APP_REBUILD=0 composer run test:coverage");
}

This is from scripts/watch-test

We’re introducing a couple of environment variables. You can manage these however you like, but my preference is to add them to composer scripts:

"scripts": {
    "test": "vendor/bin/phpunit",
    "test:cover": "vendor/bin/phpunit --coverage-html cover",
    "watch:test": "APP_COVER=0 scripts/watch-test",
    "watch:test:cover": "APP_COVER=1 scripts/watch-test",
},

This is from composer.json

APP_COVER isn’t all that important. It just tells the watcher script whether or not to include code coverage. APP_REBUILD plays a more important role: it controls whether the Pre files are rebuilt when the tests/bootstrap.php file is loaded. We need to modify that file, so that the files are only rebuilt when requested:

if (!empty(getenv("APP_REBUILD"))) {
    print "Building files" . PHP_EOL;

    deleteFilesBeforeTests(__DIR__ . "/../src");
    compileFilesBeforeTests(__DIR__ . "/../src");

    print PHP_EOL;
}

This is from tests/bootstrap.php

We also need to modify the watcher script to set this environment variable before including the bootstrap code. The whole watcher script looks like this:

#!/usr/bin/env php
<?php

putenv("APP_REBUILD=1");
require_once __DIR__ . "/../tests/bootstrap.php";

use Symfony\Component\Finder\Finder;
use Yosymfony\ResourceWatcher\ResourceWatcher;
use Yosymfony\ResourceWatcher\ResourceCacheFile;

$finder = new Finder();

$finder->files()
    ->name("*.pre")
    ->in([
        __DIR__ . "/../src",
        __DIR__ . "/../tests",
    ]);

$cache = new ResourceCacheFile(
    __DIR__ . "/.test-changes.php"
);

$watcher = new ResourceWatcher($cache);
$watcher->setFinder($finder);

while (true) {
    $watcher->findChanges();

    if ($watcher->hasChanges()) {
        $resources = array_merge(
            $watcher->getNewResources(),
            $watcher->getDeletedResources(),
            $watcher->getUpdatedResources()
        );

        foreach ($resources as $resource) {
            $pre = realpath($resource);
            $php = preg_replace("/pre$/", "php", $pre);

            print "Rebuilding {$pre}" . PHP_EOL;

            Pre\Plugin\compile($pre, $php, true, true);
        }

        if (empty(getenv("APP_COVER"))) {
            passthru("APP_REBUILD=0 composer run test");
        } else {
            passthru("APP_REBUILD=0 composer run test:cover");
        }
    }

    usleep(100000);
}

This is from scripts/watch-test

Now we should be able to launch this, and have it run our tests every time a preprocess-able file changes…

https://www.sitepoint.com/wp-content/uploads/2017/07/1500534190watcher.gif

There are a couple of things to bear in mind (rawr). The first is that you’ll need to chmod +x scripts/* to be able to run the watcher script. The second is that you’ll need to set config: {process-timeout: 0} (in composer.json) or the watcher will die after 300 seconds.

Bonus Round!

This test watcher also enables a cool side-effect: being able to use preprocessors/transformations in our PHPUnit tests. If we add a bit of code to tests/bootstrap.php:

if (!empty(getenv("APP_REBUILD"))) {
    print "Building files" . PHP_EOL;

    deleteFilesBeforeTests(__DIR__ . "/../src");
    compileFilesBeforeTests(__DIR__ . "/../src");
    deleteFilesBeforeTests(__DIR__ . "/../tests");
    compileFilesBeforeTests(__DIR__ . "/../tests");

    print PHP_EOL;
}

This is from tests/bootstrap.php

…And we enable preprocessing in our test files (for Pre that means renaming them to .pre). Then we, can start to use the same preprocessors in our test files:

namespace App\Tests;

use App\Session;
use PHPUnit\Framework\TestCase;

class SessionTest extends TestCase
{
    public function testClosureIsDecorated()
    {
        $user = ["id" => 1];
        $session = new Session($user);

        $expected = null;

        $closure = ($user) => {
            $expected = "user: " . $user["id"];
        };

        $closureWithUser = $session
            ->closureWithUser($closure);

        $closureWithUser();

        $this->assertEquals("user: 1", $expected);
    }
}

This is from tests/SessionTest.pre

Conclusion

I can’t believe I did so much preprocessor work before trying to create this kind of test watcher. It’s a testament of what we can learn from other languages and frameworks. Had I not worked on those JavaScript projects, I might have gone on rebuilding my files before each test run. Yuck!

Has this approach worked well for you? It can be adapted to work with async HTTP servers, or other long-running processes. Let us know what you think in the comments.

Frequently Asked Questions (FAQs) about JavaScript Style Test Watchers in PHP

How can I set up a JavaScript style test watcher in PHP?

Setting up a JavaScript style test watcher in PHP involves several steps. First, you need to install PHPUnit and PHPUnit-Watcher. PHPUnit is a testing framework for PHP that provides a way to write tests for your code. PHPUnit-Watcher is a tool that watches your code and runs your PHPUnit tests whenever you save a file. After installing these tools, you can configure PHPUnit-Watcher to watch your code and run your tests automatically. This setup allows you to get immediate feedback on your code changes, which can help you catch and fix errors more quickly.

What are the benefits of using a test watcher in PHP?

Using a test watcher in PHP has several benefits. It provides immediate feedback on your code changes, which can help you catch and fix errors more quickly. It also saves you time because you don’t have to manually run your tests after every code change. Additionally, it encourages you to write tests for your code, which can improve the quality of your code and make it easier to maintain.

Can I use PHP code inside a JavaScript function?

Yes, you can use PHP code inside a JavaScript function, but it’s not recommended. PHP is a server-side language, while JavaScript is a client-side language. This means that PHP code is executed on the server before the page is sent to the client, while JavaScript code is executed on the client after the page is received. Therefore, if you try to use PHP code inside a JavaScript function, the PHP code will be executed before the JavaScript function, which can lead to unexpected results.

How can I test my PHP code with Codeception?

Codeception is a testing framework for PHP that supports unit testing, functional testing, and acceptance testing. To test your PHP code with Codeception, you first need to install Codeception and configure it for your project. Then, you can write tests for your code using Codeception’s syntax and run your tests with Codeception’s command-line tool.

How can I write PHP code inside JavaScript?

While it’s technically possible to write PHP code inside JavaScript, it’s not recommended. PHP is a server-side language, while JavaScript is a client-side language. This means that PHP code is executed on the server before the page is sent to the client, while JavaScript code is executed on the client after the page is received. Therefore, if you try to write PHP code inside JavaScript, the PHP code will be executed before the JavaScript code, which can lead to unexpected results. Instead, it’s better to use AJAX to send data from the client to the server and vice versa.

Christopher PittChristopher Pitt
View Author

Christopher is a writer and coder, working at Over. He usually works on application architecture, though sometimes you'll find him building compilers or robots.

automationBrunoScompilersOOPHPPHPpreprocessorpreprocessor toolspreprocessorstddTestingunit testing
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week