You Might Not Need a Testing Framework

It depends (doesn't everything?)

It’s tempting to pull in behemoth testing frameworks even for tiny projects. But you might not need to…

I was recently working on PSR-compliant Dependency Container for when I get around to incorporating dependency injection into my plugin, wp-twig-loop-shortcode (I know, what a name). I wanted a way to easily test it, however, without having to pull in something like PHPUnit for what amounted to three very small classes (and three interfaces).

The release build for PHPUnit is over 400kb. And while a slim framework for the most part, I don’t really need the complicated functionality it provides. The tests I plan to run in it can fit in a small file.

So what to do?

Since PHP 4, the language has carried a native assert method. It allows you to test a boolean as true, throwing an exception if it’s false. In test.php, which we run via php test.php,

assert(true);
assert(false);

Will give you

Warning: assert(): Assertion failed in /home/ubuntu/workspace/DependencyContainer/Test/test.php on line 4

Call Stack:
    0.0003     248360   1. {main}() /home/ubuntu/workspace/DependencyContainer/Test/test.php:0
    0.0003     248456   2. assert() /home/ubuntu/workspace/DependencyContainer/Test/test.php:4

Ultimately, these are the only assertions you need to unit test simple classes and methods. Testing frameworks are made for complicated projects and full applications where constantly writing assertions so that they pass boolean tests becomes a huge hassle. Testing frameworks with a handful of assertions keeps your code cleaner and easier to read.

Additionally, if your project has multiple tests, a testing framework may come in handy for its ability to run many tests in one sweep.

For my purposes, I was able to fully test my dependency container in less than 100 lines of code:

require_once("../DependencyContainer.php");

$container = new DependencyContainer();

$global_scope_checker = true;

$container->inject("test", function($arguments)
{
    return  [
        "depends" =>
          $arguments["sammy"] . $arguments[0] . $arguments[1]()  
        ];
    },
    [   "sammy" => "zane",
        3,
        function() { return 42; }
    ]
);

$container->inject("test2", function($args)
{
    global $global_scope_checker;
    $global_scope_checker = false;
    return true;
});


echo "Beginning test for DependencyContainer\n\n";

assert(!$container->has("test"));
echo ".";

$dep = $container->get("test");
assert(is_array($dep));
echo ".";

assert(count($dep) == 1);
echo ".";

assert("zane342" == $dep["depends"]);
echo ".";

/**
 * This test ensures that dependencies aren't initialized until they're called,
 * to ensure that we can setup a bunch of dependencies up front, but not necessarily
 * need to load them all into memory if our request doesn't call for it.
 */

assert($global_scope_checker);
echo ".";

$dep = $container->get("test2");
assert(!$global_scope_checker);
echo ".";

assert($dep);
echo ".";

/**
 * This test ensures that the proper NotFoundExceptions are thrown
 */
try {
    $container->get("not-here");
} catch(Exception $ex) {
    assert(is_a($ex, "Exception"));
    echo ".";
    assert(is_a($ex, "DependencyContainerNotFoundException"));
    echo ".";
}

/**
 * This test ensures that exceptions are thrown properly when our injectors throw
 * errors.
 */
$container->inject("fails", function() {
    throw new Exception("nothing here");
});
try {
    $container->get("fails");
} catch (Exception $ex) {
    assert(is_a($ex, "Exception"));
    echo ".";
    assert(is_a($ex, "DependencyContainerException"));
    echo ".";
}

echo "\n\nTest suite complete! All tests pass.\n";

This returns very simple output:

$ php test.php
Beginning test for DependencyContainer

...........

Test suite complete! All tests pass.

Simple, easy to see, and doesn’t require including a testing framework for such a small project.

Continuous Integration

This method can also be used in continuous integration services like Travis CI! It takes minimal configuration.

In your .travis.yml file:

language: php
php:
  - '5.4'
  - '5.5'
  - '5.6'
  - '7.0'
script: cd Test && php test.php

My class doesn’t support PHP versions below 5.4 because prior to that, the language did not support anonymous functions. PHP 5.3 supported anonymous functions, but this implementation also required arrays that could be assigned to functions. That functionality was not added until PHP 5.4.

Now, add your Travis config file to your repo, and all tests should pass! Congrats. Now your simple module can stay tested as you make changes to it without Travis CI having to load complicated testing suites and test runners.

In Summary

Use testing frameworks if you need complicated assertions or you need to run multiple tests. For simple projects that can achieve over 100% test coverage with a few assertions, such as my DependencyContainer project, try using PHP’s built-in assert() function.