Complete Guide to automated tests for PHP applications – PHPUnit

In the first part, I’ve tried to convince you to start with automated tests, in this part we’ll finally start with the fun part of actually writing code, but before that let’s first look at some basic concept (then actually getting started, for real. 😄)

Test doubles

Sometimes it is just plain hard to test the system under test (SUT) because it depends on other components that cannot be used in the test environment. This could be because they aren’t available, they will not return the results needed for the test or because executing them would have undesirable side effects. In other cases, our test strategy requires us to have more control or visibility of the internal behavior of the SUT.

When we are writing a test in which we cannot (or chose not to) use a real depended-on component (DOC), we can replace it with a Test Double. The Test Double doesn’t have to behave exactly like the real DOC; it merely has to provide the same API as the real one so that the SUT thinks it is the real one!

The main purpose of test doubles, is to elimnate all kind of dependencies of a class or a method (your SUT) so that your tests are more simpler and focused on what you’re trying to verify.

Stubs

This is basically replacing an object with a test double that (might) returns some specific values. This stub will replace one of the component that the SUT depends on.

Mocks

Whereas in a stub, you’re creating a test double of your object, in a mock you’re basically checking that your test passes through it.

Installing PHPUnit

First create the following composer.json (make sure you have composer installed)

$ composer install

Now, you should have the executable, which will be responsible for running your tests:

./vendor/bin/phpunit

Now let’s start with some examples.

Example 1: CSSHexToDec

Let’s say, for some reason, you needed to convert hex values to their decimal equivalent, so you create this service class:

Usually, you test your code by running it in the browser or from a CLI script, like so:


$bgColor = '#ffffff';
$converter = new CssHexToDecConverter();
var_dump($converter->convert($bgColor)); // 255,255,255

Now, let’s do the same thing but using PHPUnit instead. First, we’ll create a class under a directory called ‘Test’, we’ll follow the same directory structure as for our CssHexToDecConverter class and we’ll simply append ‘Test’ to its name.

Notice that we’re extending from PHPUnit_Framework_TestCase, which is basically all we need to start using PHPUnit. We’ll explore what we will use from that base class as we go along in this series. For now, let’s remove that ugly var_dump and write our first unit test:

Now we can run the test:


./vendor/bin/phpunit src/PHPUnitDemo/Tests/Services/CssHexToDecConverterTest.php
PHPUnit 5.5.7 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 30 ms, Memory: 2.75MB
OK (1 test, 1 assertion)

Two important things to notice here, first we created a public method and adding the prefix “test” to its name. Ideally you want the name to describe as best as possible the test case you’re testing, so it’s easier to read what’s being tested (and this is why tests act as a good documentation) and also when the test fails you understand faster which case you’re dealing with. Also notice the assertEquals(mixed $expected, mixed $actual[, string $message = ”]) method provided from PHPUnit, this is basically the most useful method but PHPUnit offer much more assertion methods that are more explicit and easier to read.
Now that was a simple example as we didn’t depend on any other object. Now, let’s look at something a bit more complex.

Example 2:  iOSRequestChecker

Let’s say, the product manager wants a feature which should only accessible for people using an iOS device for some business reasons.

So you create this simple service, which relies on symfony http foundation componenet:

$ composer require symfony/http-foundation

Seems straight forward. This example is a bit different since now our service class have a dependency.

Here we created the dependency (Request object) and hardcoded some values to verify our test cases


./vendor/bin/phpunit src/PHPUnitDemo/Tests/Services/iOSRequestCheckerTest.php
PHPUnit 5.5.7 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 43 ms, Memory: 3.50MB
OK (2 tests, 2 assertions)

All green, so far so good. But if you look closley, we have some duplicated code:


$request = Request::create('foo_bar');
$iOSRequestChecker = new iOSRequestChecker($request);
$request->server->set('HTTP_USER_AGENT', 'android');

And we are also instanciating our service on both methods. Luckily, PHPUnit offers a good way to solve this. Basically, we have access to a special method where we could do any kind of setup that will be needed later on from the test:

./vendor/bin/phpunit src/PHPUnitDemo/Tests/Services/iOSRequestCheckerTest.php
PHPUnit 5.5.7 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 39 ms, Memory: 3.50MB
OK (2 tests, 2 assertions)

Now that feels better, but we can make use of more cool things from PHPUnit goodies. First, in this test, we are checking on a boolean and we have more expressive methods for that: assertTrue and assertFalse:


public function testRequestIsUsingIOS()
{
    $this->request->server->set('HTTP_USER_AGENT', 'ipad');
    $this->assertTrue($this->iOSRequestChecker->isUsingIOS());
}

public function testRequestIsNotUsingIOS()
{
    $this->request->server->set('HTTP_USER_AGENT', 'android');
    $this->assertFalse($this->iOSRequestChecker->isUsingIOS());
}

Now when you were doing tests manually, you were trying a bunch of different input everytime so we could create multiple methods with different input or we could use some nice feature from PHPUnit called dataProvider:


/**
* @dataProvider iOSDataProvider
*/
public function testRequestIsUsingIOS($iOS)
{
    $this->request->server->set('HTTP_USER_AGENT', $iOS);
    $this->assertTrue($this->iOSRequestChecker->isUsingIOS());
}

/**
* @dataProvider othersDataProvider
*/
public function testRequestIsNotUsingIOS($userAgent)
{
    $this->request->server->set('HTTP_USER_AGENT', $userAgent);
    $this->assertFalse($this->iOSRequestChecker->isUsingIOS());
}

public function iOSDataProvider()
{
    return [
        ['ipad'],
        ['iphone']
    ];
}

public function othersDataProvider()
{
    return [
        ['android'],
        ['Mozilla/4.0 (compatible; MSIE 6.1; Windows XP)']
    ];
}

Usually it’s good to seperate your use cases into different methods but here the tests seems simple and it would make sense to merge them:


/**
* @dataProvider userAgentDataProvider
*/
public function testRequestChecker($userAgent, $isIos)
{
    $this->request->server->set('HTTP_USER_AGENT', $userAgent);
    $this->assertEquals($isIos, $this->iOSRequestChecker->isUsingIOS());
}

public function userAgentDataProvider()
{
    return [
        "user is using an ipad" => ['ipad',true],
        "checking an iphone" => ['iphone', true],
        "user is using an android" => ['android', false],
        "user is using IE6" => ['Mozilla/4.0 (compatible; MSIE 6.1; Windows XP)', false]
    ];
}
./vendor/bin/phpunit src/PHPUnitDemo/Tests/Services/iOSRequestCheckerTest.php
PHPUnit 5.5.7 by Sebastian Bergmann and contributors.
.... 4 / 4 (100%)
Time: 38 ms, Memory: 3.25MB
OK (4 tests, 4 assertions)

Notice how I added a key for the data provider, I’m using that key now to compensate for the method name readability.

Now let’s suppose that your dependency (in the previous example it’s Request) have a side effect on the rest of your system, like an object that writes new entries into a database or an object that calls an API to charge the customer credit card, and so when you’re writing a unit test, you want to focus on your unit and ‘pretend’ that third party API or that database writer works as expected. So in this case, we’ll make use of stubs.

So let’s assume that there’s an imaginary API that determines if the device is an iOS device:

And our iOSRequestChecker now looks like this:

Now let’s run the test:

./vendor/bin/phpunit src/PHPUnitDemo/Tests/Services/iOSRequestCheckerTest.php
4) PHPUnitDemo\Tests\Services\iOSRequestCheckerTest::testRequestChecker with data set "user is using IE6" ('Mozilla/4.0 (compatible; MSIE...ws XP)', false)
Argument 2 passed to PHPUnitDemo\Services\iOSRequestChecker::__construct() must be an instance of PHPUnitDemo\Services\iOSApiClient, none given, called in /Users/wissem/Dev/htdocs/phpunit_tutorial/src/PHPUnitDemo/Tests/Services/iOSRequestCheckerTest.php on line 25 and defined
/Users/wissem/Dev/htdocs/phpunit_tutorial/src/PHPUnitDemo/Services/iOSRequestChecker.php:26
/Users/wissem/Dev/htdocs/phpunit_tutorial/src/PHPUnitDemo/Tests/Services/iOSRequestCheckerTest.php:25
ERRORS!
Tests: 4, Assertions: 0, Errors: 4.

Oups, forgot to add our new imaginary API so let’s do that:


public function setUp()
{
    parent::setUp();
    $this->request = Request::create('foo_bar');
    $this->iOSApiClient = new iOSApiClient('foo', 'bar');
    $this->iOSRequestChecker = new iOSRequestChecker($this->request, $this->iOSApiClient);
}
./vendor/bin/phpunit src/PHPUnitDemo/Tests/Services/iOSRequestCheckerTest.php
PHPUnit 5.5.7 by Sebastian Bergmann and contributors.
.... 4 / 4 (100%)
Time: 57 ms, Memory: 3.25MB
OK (4 tests, 4 assertions)

All green again, good. One problem though, our imaginary API will be called everytime we run our tests, so let’s see how to fix that:

Since all I care about from that API is the method “verifyIOS” with the exact output and input I’m expecting. To understand this a bit more have a look at PHPUnit documentation.

So in this part, I’ve covered how to use PHPUnit and the necessary building blocks to start writing unit tests (yes this is actually all you need to know.) But don’t worry if this still doesn’t make so much sense. In the next part, we’ll look at more examples and we’ll also see how we can start testing with Symfony. Later on this series, we’ll also have a look how to automate tests and setup some basic CI like all the other cool kids. So stay tuned.

  • Rod Elias

    Great article Wissem.

    I’m looking forward to read the next part.