Re-Introducing PHPUnit – Getting Started with TDD in PHP
There are a lot of PHPUnit posts on our site already (just check the tag), but it's been a while since we've actually introduced people to it, and the tool has evolved significantly since then.
This article aims to re-introduce the tool in a modern way, to a modern audience, in a modern PHP environment - if you're unfamiliar with PHPUnit or testing, this post is for you.
Here we assume you're familiar with object oriented PHP and are using PHP version 7 and above. To get an environment up and running which has PHP 7 pre-installed, and to be able to follow instructions in this post to the letter without getting tripped up, we suggest you use Homestead Improved. Note also that some command line usage will be expected, but you will be guided through it all. Don't be afraid of it - it's a tool more powerful than you can imagine.
If you're wondering why we're recommending everyone use a Vagrant box, I go in depth about this in Jump Start PHP Environment, but this introduction of Vagrant will explain things adequately as well.
What exactly is Test Driven Development?
Test Driven Development is the idea that you write code in such a way that you first write another bit of code the sole purpose of which is making sure that the originally intended code works, even if it's not written yet.
Checking if something is indeed what we expect it to be is called asserting in TDD-land. Remember this term.
For example, an assertion that 2+2=4 is correct. But if we assert that 2+3=4, the testing framework (like PHPUnit) will mark this assertion as false. This is called a "failed test". We tested is 2+3 is 4, and failed. Obviously, in your application you won't be testing for sums of scalar values - instead, there'll be variables which the language will replace with real values at run-time and assert that, but you get the idea.
What is PHPUnit?
PHPUnit is a collection of utilities (PHP classes and executable files) which makes not only writing tests easy (writing tests often entails writing more code than the application actually has - but it's worth it), but also allows you to see the output of the testing process in a nice graph which lets you know about code quality (e.g. maybe there's too many IFs in a class - that's marked as bad quality because changing one condition often requires rewriting as many tests as there are IFs), code coverage (how much of a given class or function has been covered by tests, and how much remains untested), and more.
In order not to bore you with too much text (too late?), let's actually put it to use and learn from examples.
The code we end up with at the end of this tutorial can be downloaded from Github.
Bootstrapping an Example Application
To drive the examples home, we'll build a simple command line package which lets users turn a JSON file into a PHP file. That PHP file will contain the JSON data as an associative PHP array. This is just a personal use case of mine - I use Diffbot a lot and the output there can be enormous - too large to manually inspect, so easier processing with PHP can come in very handy.
Henceforth, it is assumed that you are running a fully PHP 7 capable environment with Composer installed, and can follow along. If you've booted up Homestead Improved, please SSH into it now with vagrant ssh
, and let's begin.
First, we'll go into the folder where our projects live. In the case of Homestead Improved, that's Code
.
cd Code
Then, we'll create a new project based on PDS-Skeleton and install PHPUnit inside it with Composer.
git clone https://github.com/php-pds/skeleton converter
cd converter
composer require phpunit/phpunit --dev
Notice that we used the --dev
flag to only install PHPUnit as a dev dependency - meaning it's not needed in production, keeping our deployed project lightweight. Notice also that the fact that we started with PDS-Skeleton means our tests
folder is already created for us, with two demo files which we'll be deleting.
Next, we need a front controller for our app - the file all requests are routed through. In converter/public
, create index.php
with the following contents:
<?php
echo "Hello world";
You should be familiar with all the above contents. With our "Hello World" contents in place, let's make sure we can access this from the browser.
If you're using Homestead Improved, I hope you followed instructions and set up a virtual host or are accessing the app via the virtual machine's IP.
Let's delete the extra files now. Either do it manually, or run the following:
rm bin/* src/* docs/* tests/*
You may be wondering why we need the front controller with Hello World. We won't be using it in this tutorial, but later on as we test our app as humans, it'll come in handy. Regardless, it won't be part of the final package we deploy.
Suites and Configurations
We need a PHPUnit configuration file which tells PHPUnit where to find the tests, which preparation steps to take before testing, and how to test. In the root of the project, create the file phpunit.xml
with the following content:
<phpunit bootstrap="tests/autoload.php">
<testsuites>
<testsuite name="converter">
<directory suffix="Test.php">tests</directory>
</testsuite>
</testsuites>
</phpunit>
phpunit.xml
A project can have several test suites, depending on context. For example, everything user-account-related could be grouped into a suite called "users", and this could have its own rules or a different folder for testing that functionality. In our case, the project is very small so a single suite is more than enough, targeting the tests
directory. We defined the suffix
argument - this means PHPUnit will only run those files that end with Test.php
. Useful when we want some other files among tests
as well, but don't want them to be run except when we call them from within actual Test files.
You can read about other such arguments here.
The bootstrap
value tells PHPUnit which PHP file to load before testing. This is useful when configuring autoloading or project-wide testing variables, even a testing database, etc - all things that you don't want or need when in production mode. Let's create tests/autoload.php
:
<?php
require_once __DIR__.'/../vendor/autoload.php';
tests/autoload.php
In this case, we're just loading Composer's default autoloader because PDS-Skeleton already has the Tests namespace configured for us in composer.json
. If we replace template values in that file with our own, we end up with a composer.json
that looks like this:
{
"name": "sitepoint/jsonconverter",
"type": "standard",
"description": "A converter from JSON files to PHP array files.",
"homepage": "https://github.com/php-pds/skeleton",
"license": "MIT",
"autoload": {
"psr-4": {
"SitePoint\\": "src/SitePoint"
}
},
"autoload-dev": {
"psr-4": {
"SitePoint\\": "tests/SitePoint"
}
},
"bin": ["bin/converter"],
"require-dev": {
"phpunit/phpunit": "^6.2"
}
}
After this, we run composer du
(short for dump-autoload
) to refresh the autoloading scripts.
composer du
The First Test
Remember, TDD is the art of making errors first, and then making changes to the code that gets them to stop being errors, not the other way around. With that in mind, let's create our first test.
<?php
namespace SitePoint\Converter;
use PHPUnit\Framework\TestCase;
class ConverterTest extends TestCase {
public function testHello() {
$this->assertEquals('Hello', 'Hell' . 'o');
}
}
tests/SitePoint/Converter/ConverterTest.php
It's best if the tests follow the same structure we expect our project to have. With that in mind, we give them the same namespaces and same directory tree layouts. Thus, our ConverterTest.php
file is in tests
, subfolder SitePoint
, subfolder Converter
.
The file we're extending is the most basic version of the Test class that PHPUnit offers. In most cases, it'll be enough. When not, it's perfectly fine to extend it further and then build on that. Remember - tests don't have to follow the rules of good software design, so deep inheritance and code repetition are fine - as long as they test what needs to be tested!
This example "test case" asserts that the string Hello
is equal to the concatenation of Hell
and o
. If we run this suite with php vendor/bin/phpunit
now, we'll get a positive result.
PHPUnit runs every method starting with
test
in aTest
file unless told otherwise. This is why we didn't have to be explicit when running the test suite - it's all automatic.
Our current test is neither useful nor realistic, though. We used it merely to check if our setup works. Let's write a proper one now. Rewrite the ConverterTest.php
file like so:
Continue reading %Re-Introducing PHPUnit – Getting Started with TDD in PHP%