Backend Coder Logo

Easy Testing of PHP

Published: 5th Nov 2016

I have created an easy to use and quick to learn PHP Testing Framework to do command-line unit testing of your PHP code modules. The code repository may be found here: PHP Tester

A Test Spec is created for each class that is to be tested. This is a PHP file with a list of methods and their associated tests. It also contains functions that perform the tests. You also need to create Mock Classes that simulate dependencies.

Here is an example class to be tested:


class Math
{
	public $property;

	public function __construct()
	{
		$this->property = 0;
	}

	public function sum($a, $b)
	{
		return $a + $b;
	}

	public function multiply($a, $b)
	{
		return $a * $b;
	}

	public static function getPI()
	{
		return 6.7; // Bug
	}

	public static function useHelper()
	{
		return Helper::sayHi();
	}

	public function displaySum($a, $b, $c, $d = 1)
	{
		echo ($a + $b + $c + $d);
	}
}

This class has a public property, and various methods including a static method. The getPI method has been set up to produce a Failed test. Setting the value to 3.141 will allow the test to pass.

Here is a Test Spec written in PHP for the above Class:


// A list of methods and their associated test function names and descriptions. Separate name and description with space/tab characters
$spec = "
__construct
get_instance			It should provide an instance of the class
sum
it_should_add_integers	It should add integers
it_should_add_floats	It should add floats
multiply
it_should_multiply		It should multiply numbers
getPI
static_method It should support static methods
useHelper
use_mock_class		It should be able to access dependencies (other classes)
displayNumber
display_a_number	It should display a number
";

// Test function definitions
function get_instance()
{
	// To keep your learning curve short and sweet there is only one testing method: compare(number/bool/array/object/string A , number/bool/array/object/string B)
	Tester::compare(new Math(), '
Math Object
(
    [property] => 0
)
');
}

function it_should_add_integers()
{
	// Always start from a fresh instance of the class that is being tested to avoid previous effects, and use this Template of code to save you work
	$instance = new Tester::$class(); // Here we are using the stored value of the Class name

	Tester::compare($instance->sum(1,2), 3);
}

function it_should_add_floats()
{
	$instance = new Tester::$class();

	Tester::compare($instance->sum(0.5,0.3), 0.8);
}

function it_should_multiply()
{
	$instance = new Tester::$class();

	Tester::compare($instance->multiply(2,6), 12);
}

function static_method()
{
	Tester::compare(Math::getPI(), 3.141); // Have to use the actual Class name for static methods
}

function use_mock_class()
{
	Tester::compare(Math::useHelper(), 'Hello!'); // This useHelper method will cause the Tester's Class Autoloader to load the Mocked Helper class.
}

function display_a_number()
{
	Tester::compare(Tester::get_displayed_content('display_sum', [1, 2, 3]), 7);
}

function display_sum($a, $b, $c)
{
	(new Math())->displaySum($a, $b, $c); // Here I am showing off a technique to directly access an instantiation of a class, you could show off more with a variable class name maybe :)
}

At the top is the list of tests. This is plain text where each method name is one word on a line. The method name is then followed by one or more function name and a description pairs on each line. You don't have to be fussy about using tabs or spaces for indentation of the descriptions.

The function name refers to the function below that actions the test. The function will set up the initial conditions and then call the method to produce a result. The result may be a returned value or a display of the value to the window.

A displayed value needs to be wrapped so that the displayed content may be returned as a value. For this, we have the Tester::get_displayed_content() helper method.

This method takes a function name parameter and optionally a single parameter value or array of parameter values.

Each test involves comparing the result with an expected value. To do the test we have the Tester::compare() method.

This method takes parameters of a result value and an expected value. You can see how it works in the code examples above. Objects are serialized in the comparison so that you can specify the expected result as text or as an object, it's up to you.

The display_a_number() function demonstrates the way we wrap displayed results before passing them to the comparison method.

When you are ready to run tests on a Class, you execute: php tester.php Class from the command line where Class is the name of the Class such as Math. But first specify the path to your classes in the config.php file.

The Spec files should be named as classSpec.php to be automatically loaded.

The tests will either cause a report of they passed, or there was a Failure (with a report of what failed), or there is a Fatal error in your code that stops the code running.

Testing Exceptions

It's a bad idea to halt your script execution with an exit or die statement since this will stop the test runner. It's better to throw an Exception where you can convey a meaningful message to the user, have the error logged, and also capture errors in the unit tests.

So in your test code you should wrap tests in a try{} catch(){} block where you are testing the operation of throwing an error.

For example:


function handler_not_set()
{
	try
	{
		(new Data())->get('x');
		Tester::compare('', 'No exception was thrown!');
	}
	catch(Exception $e)
	{
		Tester::compare($e->getMessage(), 'Data handler instance not set!');
	}
}

In the try block we execute the function under test with data/parameters to trigger the error condition, and in this block we use the Tester::compare method to indicate that an exception was not thrown if that is the case. In the catch block that follows, we compare the Exception error message to what we expect for the test to pass. And, then the tests will continue to run.

Tips

When building up your test Spec file it is best to add one test at a time starting with the most simple methods to test or methods that don't depend on other things.

Also, add the next test function above the last one that you added so that you don't need to scroll up and down the page as you build up your test Spec.

In the command line console window you can quickly execute the tests from the command line and use up-arrow to repeat the same command.

To easily cut and paste expected results such as an object/array you can pipe the output to a file such as temp.txt e.g. php tester.php MyArrayProductionClass > temp.txt.

Also I recommend setting up a project in Sublime Text Editor where your test project folder is added to the project. This makes it quick to edit the files and navigate around your project folders.

Once you have set up multiple Spec files, you can run them as a batch using a batch file. I provide an example called run-tests.bat and also a generator (make-bat) to scan for Spec files to build this file automatically.

Sometimes you have a class that does something that does not return a result but you need to know if it did it's job. In this case, a Mock Class can set the static property: Tester::$result which will be visible to your test functions. This may not make sense right now, but when you come across this need, it is catered for!

You may have code modules in your project that are in different folders. In this case you test the code in one folder such as initially defined in the config.php file and then change the value of Tester::$classPath in the first test function of code that is in the new location.

Tests may be performed on procedural PHP code as well. Here the Class name parameter of the command line is the file name without it's extension e.g. if your code is in mycode.php then the command line is: php tester.php mycode and your test Spec file name is: mycodeSpec.php

Avoid using die or exit statements in your code since these will terminate the testing code execution. It's better to have say an Abort mechanism that populates an error object and returns false rather than true as a result for example.

You can add Mock Classes directly into the test spec files that stop the Class Autoloader from loading the shared version. This allows for tailoring of the Class to the specific needs of the particular tested Class. Also it may be easier to maintain rather than having shared Mock Classes for your situation.

You should install and use Kint Class to help with debugging as well. In fact we include it if available, and simulate the d and ddd functions if not.

Internationalization

In the code I use tokens for readable text displayed to the user, this is to facilitate Internationalization (support for other spoken languages). You may translate the text in the config.php file.

Notes

You may be wondering why there is only one comparison function and not a whole suite of conversion and comparison functions built in. Well that is because these extras are very easy to implement by yourself in your test functions and saves having to learn them and keep referring to documentation. We are keeping things as simple as possible here so that no training course is needed.

You may have noticed that I coded another PHP Testing Framework which was aimed at outputting Javascript to feed into a Jasmine Test Runner. This is cool, but I now think that this new one is so easy and quick to use that it's better to separate your Javascript and PHP testing. You could easily run both in tandem from your build tool. Also, the new test Spec format is simpler I think.

Hopefully this will work on all platforms since it uses PHP and PHP_EOL for the console output line endings.

Well I hope that you have fun trying out this PHP Testing Framework and please mention it to colleagues and post about it on social media!

It's open source (MIT Licence) at: PHP Tester