Getting started with PHPUnit
PHPUnit is a testing framework that can do a lot of things all related to testing and reports about our code. Keep in mind this is an introduction and hopefully, the first part of a series about testing. Mocking, coverage, annotations and reports are concepts that will not be introduced here.
PHPUnit is mainly famous for being the standard way to write unit tests for
PHP applications, of course there are other types of tests that PHPUnit can
handle but here we’ll just look at unit tests, that means having a test (or
multiple) for every one of our functions. A common approach to structure tests
in our project is to mirror the src
directory structure in the tests
folder, whenever I start a PHP project I setup the following directory
structure:
- src/
- tests/
- phpunit.xml
- composer.json
The src
folder is where all of our application code lives, inside this
folder we use the PSR-4 standard to organize our namespaces.
Of course the tests
folder is where all our test code lives, as I’ve said,
this folder mirrors the structure of the src
folder, but all the tests use
the same namespace that the application code uses, some people prefer to have
them in a separated namespace and that’s fine, it’s just a matter of
preferences.
The phpunit.xml
file is where we should store all configurations, flags and
arguments that I want to pass to the phpunit
command, we’ll see more about
this in short.
As of the composer.json
I’ll not explain what it is, if you’re reading this
it’s very likely you already know about Composer.
Making a simple Json parser app
It’s better to explain testing on a example, so we’re going to build a simple app that can parse an object to json (serialize).
Let’s require phpunit
in our project:
$ composer require phpunit/phpunit --dev # Pull phpunit for 'dev' environment only
Inside our composer.json
let’s define our namespace, following the best
practices we need a vendor name (Antares will do just fine) and a
package name (I’ll choose Serializers). Remember that the vendor is
something unique that will identify all of your packages, as a popular rule
you should use your Github username or your domain name.
Ok, let’s add the autoload
object:
...
"autoload": {
"psr-4": {
"Antares\\Serializers": "src/"
}
}
...
Note: Every time you add a new class or change the namespace definition run
composer dump-autoload
to refresh the autoloader.
With that we just tell composer that the code inside src
should be under
the Antares\Serializers
namespace.
Now put this in to the phpunit.xml
file:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="tests/bootstrap.php">
<testsuites>
<testsuite name="Antares serializers Test Suite">
<directory>tests/</directory>
</testsuite>
</testsuites>
</phpunit>
As I said earlier, this file passes options to be used by default when
running phpunit
. The main thing to grasp here is the <testsuite>
tag
definition, this tells phpunit
that our test files are located in the
tests/
directory, thanks to this we don’t have to specify the folder as an
argument every time we run the tests.
Another important definition in this file is the bootstrap
attribute, this
tells to phpunit to run that file to setup our test suite, and as the name
implies the code in that file will serve as a bootstrapping mechanism.
Let’s create this tests/bootstrap.php
file now and put this in there:
<?php
// tests/bootstrap.php
// Get the path to the autoload file
$autoload = dirname(__DIR__) . '/vendor/autoload.php';
// The autoload file only exists if "composer install" was run
if (! file_exists($autoload)) {
exit("Please install project by running:\n\tcomposer install\n\n");
}
// Get the loader by including the autoload file
$loader = include $autoload;
// Add the tests directory to the "Antares\Serializers" namespace
$loader->addPsr4('Antares\\Serializers\\', __DIR__);
As you can read in the comments all we do in this file is add our tests
directory to the Antares\Serializers
namespace. This will only happen when
running the tests since it’s a bad practice to have our tests loaded in our
application namespace when we are not developers testing the code (i.e. when
the application is in production mode).
Since we’re using TDD lets define our expectations in a test file, go ahead
and create the tests/JsonSerializerTest.php
file, with the following code
in it:
<?php
namespace Antares\Serializers;
class JsonSerializerTest extends \PHPUnit_Framework_TestCase
{
}
The key point here is that all test classes along with the test filename must
end with the word Test
, better yet if we use the same name in both the test
file and the test class. At this point see what happens when we run
phpunit
:
$ vendor/bin/phpunit
Sample output:
PHPUnit 5.1.4 by Sebastian Bergmann and contributors.
W 1 / 1 (100%)
Time: 42 ms, Memory: 3.25Mb
There was 1 warning:
- Warning
No tests found in class “Antares\Serializers\JsonSerializerTest”.
WARNINGS! Tests: 1, Assertions: 0, Warnings: 1.
We can see no tests were found. Tests are just functions defined inside a
TestCase
class, let’s write our first test. The idea is simple, we’re going
to create an instance of the JsonSerializer
class (which at the moment
isn’t defined) and we’ll call the serialize
method on that instance
expecting to pass an array and getting back a json representation of that
array, this is the behaviour we’re going to test.
Inside the test class write the following function (which we’ll be considered a test):
<?php
namespace Antares\Serializers;
class TestCase extends \PHPUnit_Framework_TestCase
{
public function testSerialize()
{
// Create a JsonSerializer instance
$jsonSerializer = new JsonSerializer();
// Define a sample array for testing the serializer
$subject = [
'popular-languages' => [
'javascript', 'php', 'ruby', 'python'
]
];
// Define how the string should look after serialization
$jsonSubject = '{"popular-languages":["javascript","php","ruby","python"]}';
// Serialize the sample array
$serialized = $jsonSerializer->serialize($subject);
// Ensure the serialized array looks equals to the $jsonSubject string
$this->assertEquals($jsonSubject, $serialized);
}
}
We should note a couple of things here, first, any test we’ll write is a
function whose name should start with the test
word, camelCase is
preferred in the PHP community so testSerialize
is a valid name, more over
serialize
is the name of the method we want to test so it makes sense to
name our test this way. Second, PHPUnit includes a lot of defined assertions,
here we only use assertEquals
but I encourage you to take a look in the
assertions page.
If you run the test of course it will fail because the JsonSerializer
class
doesn’t exist just yet, let’s create that class in our src
folder.
<?php
// src/JsonSerializer.php
namespace Antares\Serializers;
class JsonSerializer {}
Run phpunit again, and it will fail again, now it doesn’t find the
serialize
method, lets add it:
<?php
namespace Antares\Serializers;
class JsonSerializer
{
public function serialize($structure)
{
//
}
}
Tip: You can temporary alias vendor/bin/phpunit
to t
with the command
alias t="vendor/bin/phpunit"
. From now on just type t
to run the tests.
Once more run the test, and we see the first failing test:
PHPUnit 5.1.4 by Sebastian Bergmann and contributors.
F 1 / 1 (100%)
Time: 31 ms, Memory: 3.75Mb
There was 1 failure:
- Antares\Serializers\TestCase::testSerialize
Failed asserting that null matches expected ‘{“popular-languages”:[“javascript”,“php”,“ruby”,“python”]}’.
_Users_antares_demo_tests/JsonSerializerTest.php:21
FAILURES! Tests: 1, Assertions: 1, Failures: 1.
Notice the format of the message “Failed asserting that x matches expected
y” this tells you about the order of the arguments in the assertions. In
PHP, all functions return something, if a return statement isn’t explicitly
declared then null
will be returned implicitly.
The easy way to make the test pass is to return the desired string:
// src/JsonSerializer.php
public function serialize($structure)
{
return '{"popular-languages":["javascript","php","ruby","python"]}';
}
Now the test passes but this is a dummy solution, lets add another test to avoid this solution:
// tests/JsonSerializerTest.php
public function testSerialize()
{
$jsonSerializer = new JsonSerializer();
$subject = [
'popular-languages' => [
'javascript', 'php', 'ruby', 'python'
]
];
$subject2 = ['friday', 'saturday', 'sunday'];
$jsonSubject = '{"popular-languages":["javascript","php","ruby","python"]}';
$serialized = $jsonSerializer->serialize($subject);
$weekends = $jsonSerializer->serialize($subject2);
$this->assertEquals($jsonSubject, $serialized);
$this->assertEquals('["friday","saturday","sunday"]', $weekends);
}
This extra assertion will cause our test to fail, so lets fix the serialize
function to make it pass for both assertions.
// src/JsonSerializer.php
public function serialize($structure)
{
return json_encode($structure);
}
Run the test for the last time (in this exercise) and now it passes.
PHPUnit 5.1.4 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 26 ms, Memory: 3.50Mb
OK (1 test, 2 assertions)
Ok so for the time being I think this is enough to get you started with PHPUnit, I hope to make a follow up in the future, but you never know.
I encourage you to dive in the PHPUnit documentation to know all of the assertions available in the framework.