Some tips for better PHPUnit tests

Writing good unit tests can be a pain. When you write your tests superfluously, you're testing more than the single unit, and they start failing with reasons not related to the Subject Under Test (SUT). Or your tests become unreadable, and you can't figure out what they were supposed to be testing. Or they test only the one situation where everything goes according to plan, and not possible errors. Of course we want to mitigate all that! Let's see some simple tips to keep your test suite clean and working all the time.

Test the unhappy path first

You could've sworn that your test was complete, but there seem to be exceptional cases that you missed. When writing unit tests -and doing TDD in particular- thinking about what might go wrong is very important: how should your unit fail, and when? What should happen when it fails? Should it throw exceptions? If you start with this, you won't forget to do it, and you'll write more robust code as a side-effect.

Assertions have a structure

You might not know it, but most PHPUnit assertions have their parameters in the same order. They almost always take $expected and $actual as first and second parameters respectively. The third (and optional) parameter is often $message, which is a specific message you can write to make PHPUnit rapport back to you with when something fails. This is important, because in case of failure, PHPUnit will report you that your assertion's actual value doesn't match what you expected (so order is significant here). Let's see some examples:

$this->assertEquals($expected, $actual, $message);
$this->assertSame($expected, $actual, $message);
$this->assertTrue($condition, $message);

As you can see, the assertTrue() call misses an $expected part, as it's already made clear in the name of the assertion.

Use the correct assertion

To me, the daddy of all assertions is assertEquals, it's the basis of a unit test. You assert that your Subject Under Test gives a return value that's equal to what you expect it to return. If you're in doubt about which assertion is the right one to use, always pick assertEquals. The added benifit you get, is that PHPUnit displays really nice diffs between the $expected and $actual values if they don't match. That's much less useful when you use assertTrue, and it can only tell you that False is not equal to True.

Diff that PHPUnit shows when expected is different than the actual value

Pick names that make sense

PHPUnit expects you to start the name of your test methods with test, so that you get names like testItThrowsAnExceptionWhenNetworkIsDown(). Although that name is quite descriptive (and it should be!) it doesn't read very well. We can, however, make much more readable function names using underscores. What do you think of test_it_throws_an_exception_when_network_is_down()? It might not fit your coding standard (i'm looking at you, PSR-2), but isn't readability worth the occasional exception to some rules that are meant for readability? Also note, that be reading the name of the test method like this, it's totally clear to me what that test does, and I don't have to go in and read the code if I don't need to change it.

Data Providers for clear failure messages

If you need to test the same method with a huge number of inputs and make the same assertion over and over again, you might be tempted to write your assertion inside a foreach loop or something similar.

public function test_prices_cast_to_strings()
{
    $validPrices = array(
        '$100' => new Price(10000),
        '$10' => new Price(1000),
        '$1' => new Price(100),
        '$0.10' => new Price(10),
        '$0.01' => new Price(1),
    );

    foreach ($validPrices as $expectedString => $price) {
        $this->assertEquals($expectedString, (string) $price);
    }
}

The problem with this, is that all your assertions happen on the same line, and PHPUnit will rapport a failure in one of those assertions as a failure on that line. Then you'll have to play detective and start looking through your test data to see where things break. This is where PHPUnit Data Providers come in handy, check it out:

/**
 * @dataProvider validPrices
 */
public function test_prices_cast_to_strings($expectedString, $price)
{
    $this->assertEquals($expectedString, (string) $price);
}

public function validPrices()
{
    return array(
        array('$100', new Price(10000)),
        array('$10', new Price(1000)),
        array('$1', new Price(100)),
        array('$0.10', new Price(10)),
        array('$0.01', new Price(1)),
    );
}

Now PHPUnit will tell you which input data made the test fail. Don't you think this also cleans up the test so much more? No more foreach that didn't add any meaning to the test! What's also good to know, is that the @dataProvider annotation also works in conjunction with the @expectedException annotation, so you can also easily test if your method throws exceptions for invalid input.

/**
 * @dataProvider invalidPrices
 * @expectedException InvalidArgumentException
 */
public function test_prices_throw_when_built_from_non_integer_value($nonInteger)
{
    $price = new Price($nonInteger);
}

public function invalidPrices()
{
    return array(
        array('foo'),
        array(null),
        array(true),
        array(false),
        array(0.01),
        array('1'),
    );
}

Programming against interfaces

When you're writing tests for a SOLID codebase, be it in a test-first fashion, or writing tests after the facts, you'll be glad you used interfaces for your Subject Under Test's dependencies. Not only do they give you the obvious benefits of "being able to swap out dependencies with a totally different implementation" or "not caring about specifics, if you just adhere to the contract", but also this means that you can test your class without its dependencies! Look at this example, where Bank has two dependencies:

<?php

final class Bank
{
    private $clock;
    private $log;

    public function __construct(Clock $clock, TransactionLog $log)
    {
        $this->clock = $clock;
        $this->log = $log;
    }

    // ...
}

Of course, Bank cannot operate without its dependencies, so in our test we'll have to instantiate it correctly, but we'll use test doubles, so that we can test Bank with e.g. a Clock that we control:

$clock = $this->getMock('Clock');
$clock
    ->method('getTime')
    ->willReturn(new DateTime('2017-07-15 10:00:00'));

$log = $this->getMock('TransactionLog');

$bank = new Bank($clock, $log);

Now that we know that Clock will always return a given time, we can make assertions that e.g. a Bank transaction was done at the time the clock returned, instead of taking a guess and using stuff like time() or new DateTime('now'). On the TransactionLog, we could assert that a certain method got called (because we expect a transaction to be made), and thus validate if the Bank communicates correctly with it:

$now = new DateTime('2017-07-15 10:00:00');
$clock = $this->getMock('Clock');
$clock
    ->method('getTime')
    ->willReturn($now);

$transaction = new Transaction('Toon', 'Hans', 100, $now);
$log = $this->getMock('TransactionLog');
$log
    ->expects($this->once())
    ->method('log')
    ->with($this->equalTo($transaction));

$bank = new Bank($clock, $log);
$bank->payment('Toon', 'Hans', 100);

On the last line we do the call that your code would actually do to make a bank transaction (this is example code, beware). This makes the Bank log a Transaction in the TransactionLog. Since we control the clock, we can assert on the correct time in the Transaction. Also note that this is a complete test. The rather invisible assertion is the $this->equalTo() call. We're asserting that Bank communicates correctly to the TransactionLog.

Interface Discovery

When we're doing TDD, the PHPUnit test doubles can help us "discovering" what an interface must be, in order to have a dependency that properly supports our Subject Under Test. This works because PHPUnit's mocks will mock any non-existing interfaces. Let's say we want the bank to trigger a WebHook whenever a transaction was made, containing the transaction details.

$clock = $this->getMock('Clock');
$log = $this->getMock('TransactionLog');

// The WebHooks interface doesn't exist yet
$webhooks = $this->getMock('WebHooks');
$webhooks
    ->expects($this->once())
    ->method('trigger')
    ->with($this->equalTo('{"transaction": {"from": "Toon", "to": "Hans", "amount": 100}}'));

$bank = new Bank($clock, $log, $webhooks);
$bank->payment('Toon', 'Hans', 100);

Without having an interface, we can already define what we want to see happening. Our test then fails, because the trigger() method didn't get called, so we can start implementing. When we're done, we'll see that our Bank class tests green, and we have discovered a very simple interface for the WebHooks that we can now create (see a whole blog post about this in the Further Reading section).

<?php

interface WebHooks
{
    public function trigger($jsonContents);
}

Further Reading

These were only a few of the things that you can do to make your testsuite better than it is right now! There are lots of ways to improve even further. My strategy is to not only refactor the code (when tests are green of course), but also the tests, to make them as comprehensive and robust as possible. Think about how you can make them easy to understand for the next person looking to improve or change them. Here's some more reading material that can help you out:

That's it, enjoy writing better tests!

Categories: Testing