Written by Maciej Kosiedowski
Published March 26, 2018

How to improve the quality of your PHP code? Part 2 – Unit testing

In the previous part of “How to improve my PHP code?” we set up some automated tools to automatically review our code. That’s helpful, but it doesn’t really give us any impression on how our code meets the business requirements. We now need to create tests specific to our code’s domain.

Improve your PHP Code - Unit Testing

Haven’t read the first part about automated tools? Check it out first.

Unit testing

Probably most common way of testing software is writing unit tests. They serve the purpose of testing particular units of the code, basing on the assumption that everything else works as expected.

In order to be able to write proper unit tests our code should follow some basic design rules. We should especially focus on SOLID principles.

In unit tests we really want to replace all dependant services with mock objects, so we only test one class at a time. But what are mocks? They are objects implementing the same interface as some other object, but behaving in a controlled way. For example, let’s say we are creating a price comparison service and we utilize another service to fetch current exchange rates. During testing our comparator we could use a mock object configured to return specific rates for specific currencies, so our test would neither be dependant nor call the real service.

Which framework should you use?

There are several good frameworks that serve this purpose. Most commonly known is probably PHPUnit. During my work I found out that using behavioral approach to writing tests gives better results and makes me to write tests more eagerly. For our project let’s chose phpspec.

The setup is fairly easy – you just install it using:

php composer.phar require --dev phpspec/phpspec

Then, if you are using PHing configured in first part of the article, you add build target to build.xml:

<target name="phpspec">
    <exec executable="bin/phpspec" passthru="true" checkreturn="true">
        <arg line="run --format=pretty"/>
    </exec>
</target>
...
<target name="run" depends="phpcs,phpcpd,phan,phpspec"/>

Then you have to create a test class for every service class you want to test. What makes PHPSpec extremely easy to use is mock-creation. You just declare mock objects as the test function’s parameters, using strict typing. PHPSpec will automatically create mocks for you. Let’s look at the code example:

// spec/Domain/PriceComparatorSpec.php
<?php

namespace spec\Domain;

use Domain\Price;
use Domain\PriceConverter;
use PhpSpec\ObjectBehavior;

class PriceComparatorSpec extends ObjectBehavior
{
    public function let(PriceConverter $converter)
    {
        $this->beConstructedWith($converter);
    }

    public function it_should_return_equal()
    {
        $price1 = new Price(100, 'EUR');
        $price2 = new Price(100, 'EUR');
        $this->compare($price1, $price2)->shouldReturn(0);
    }

    public function it_should_convert_first(PriceConverter $converter)
    {
        $price1 = new Price(100, 'EUR');
        $price2 = new Price(100, 'PLN');
        $priceConverted = new Price(25, 'EUR');
        $converter->convert($price2, 'EUR')->willReturn($priceConverted);
        $this->compare($price1, $price2)->shouldReturn(1);
    }
}

There are three functions here:

  • let() – it allows you to initialize your service with dependencies
  • two it_* functions implementing tests. One of them is using mock $priceConverter implementing PriceConverter interface which was injected to the tested object on it’s creation.

You can see that creating mock is very easy. All you need to do is define it as test function’s parameter and configure the mock by specifying which functions should be run during execution of your code. If this is needed,  you can also set their return value.

All tested methods are run from $this context and you can easily check their result using same syntax as you use with mocks.

How to set up the tests?

Phpspec comes with a very nice documentation, but I will try to show you some basic use cases useful in daily practice.

Constructing test object
Generally the easiest way to set up test object is calling $this->beConstructedWith(…) method which takes as parameters all the params that should be passed to your object’s constructor.

If your object should be created using a factory method, you can use $this->beConstructedThrough($methodName, $argumentsArray) method.

Matching runtime parameters in mocks
You will find that phpspec uses a very human-like syntax for configuring mocks. Eg. if you want to check if a mock method someMethod is called in runtime with the parameter “desired value”, you define it in your test like in the following example:

$mockObject->someMethod("desired value")->shouldBeCalled();

If you want to test your code’s behavior when some mock’s function someFunction returns “some value”, you can easily set it up by calling:

$mockObject->someFunction("some input")->willReturn("some value");

Sometimes you don’t really care about the exact parameter passed to a mock. You can then write this code:

use Prophecy\Argument\Token\AnyValueToken;
...
$mockObject->someFunction(new AnyValueToken())->willReturn(true);

Sometimes you care about some parameter so much that it’s better to write a check function which will tell if some method was called properly, eg.:

use Prophecy\Argument\Token\CallbackToken;
...
$checker = function (Message $message) use ($to, $text) {
   return $message->to === $to && $message->text === $text;
};
$msgSender->send(new CallbackToken($messageChecker))->shouldBeCalled()

Matching exceptions in runtime
In some cases exceptions are part of your code’s interface. You want them to be thrown in specific scenarios and test those scenarios. You can do this with phpspec by writing the following code:

$this->shouldThrow(\DomainException::class)->during('execute', [$command, $responder]);

The first argument passed to during() is the name of a method that will be called, the second one is an array of parameters that will be passed to our method.

Where to find more examples?

In this article I only covered some basic use cases. Please refer to phpspec’s documentation to find more examples that will make your test code beautiful!

Code coverage

PHPSpec comes with extension subsystem which allows eg. to create code coverage reports. They are helpful if you want to check how big part of your code is executed in tests.

You can install this extension by running:

php composer.phar require --dev leanphp/phpspec-code-coverage

And then enabling it by creating phpspec.yml file with content:

extensions:
  LeanPHP\PhpSpec\CodeCoverage\CodeCoverageExtension: ~

By default, this extension generates code coverage information using PHP’s Xdebug extension, however they run much faster with PHP’s native debugger – phpdbg:

$ phpdbg -qrr phpspec run

You can now change your build target for phpspec in build.xml to:

<target name="phpspec">
    <exec executable="phpdbg" passthru="true" checkreturn="true">
        <arg line="-qrr bin/phpspec run --format=pretty"/>
    </exec>
</target>
...
<target name="run" depends="phpcs,phpcpd,phan,phpspec"/>

Reports are generated in coverage/ directory as nice HTML pages that can be browsed to check test coverage.

When should you write unit tests?

The answer is… as often as possible! They run very fast and they are probably by far the easiest way of checking your code. It’s the best approach that rewards you with hours not spent on manual testing, and instead it’s performed within seconds. Unit tests are especially helpful in a non-compilable code, like PHP. They help to catch the problems which otherwise would occur only in runtime.

Writing tests will also help you to write a better organized code. Think about testing? Create a testable code structure.

Summary of part II

That’s all for today! Now it’s your time to grab your keyboard, write some tests for your project and feel confident about your code like never before! In the next part we will finish our test stack by adding integration / end-to-end tests.

Read the last part of the series!

Written by Maciej Kosiedowski
Published March 26, 2018