How to Write JavaScript-Style Test Watchers in PHP
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.
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.php
⇒ path/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.
Continue reading %How to Write JavaScript-Style Test Watchers in PHP%