TailTemplate Build stunning websites faster with our pre-designed Tailwind CSS templates

Modern PHP Developer - TDD

If you have not heard of Test Driven Development(TDD), you should begin to familiarise yourself with it. Though PHP community is a bit late on TDD practice compared to other languages such as Ruby, once the benefits TDD were realized, it has become almost essential for a modern PHP developer.

TDD is a software development technique. The basic idea behind TDD is that, we create tests before we actually code any thing. Writing test against no code is more of a mindset shift than anything else. It is opposite of traditional coding habit, where we create code first, then manually run the unit to make sure it does what we intended manually. The benefits that TDD brings to us are enormous. At first it forces us to think about code design before we create any concrete code, then it allows us to refactor our code base without worrying about the side effect. It makes our code easy to maintain in the long run.

TDD consists of three phases: which are Red, Green and Refactor.

Red phase

In red phase, as the developer, we will plan out what the code will look like without actually writing it. This is to say, we will design our class or class methods, without implementing its details. Initially this phase is hard, it requires us to change our traditional habit of coding. But once we get used to this process, we will naturally adapt to it and realize that it helps us design better code. It is about changing our mindset, as we should focus on the input and output of the API, instead of the details of the code. The result of this phase is successful creation of red test.

Green phase

In green phase, it is all about writing the quickest piece of code to pass the tests. In this phase, we should not spend too much making the code clean or refactoring. Though we all want to write the most beautiful piece of code, that is not the task at hand in this phase. The result of this phase is green tests.

Refactor phase

In refactor phase, we focus on making the code clean. Since we have tests created above to guard bugs from side effects, we gain confidence for carrying out refactor. If by chance, a bug is introduced from refactoring, our tests will report it as soon as it appears. So the natural way of refactor is to run the test as soon as you have modified any code.

PHPUnit

TDD lets us test drive our development cycle. When practicing TDD in PHP, obviously we need to define the kind of test we will do. The most common test in TDD is Unit Test which tests the smallest testable parts of an application it considers a unit, which is typically a class method.

Now imagine writing unit tests manually and building an automated method to run them. It is definitely a lot of work. Fortunately, there are already unit testing frameworks out there for us to use. Among a number of unit testing frameworks, PHPUnit is the most popular one and it is widely used in the PHP community.

Getting started with PHPUnit

Installation

The easiest way to install PHPUnit is via Composer. Open up your terminal and in your project folder, simply run composer require phpunit/phpunit . By default, the bin file of PHPUnit will be placed into vendor/bin folder, so we can run vendor/bin/phpunit directly from our project's root folder.

Your first unit test

Time to create your first unit test! Before doing so, we need a class to test. Let's create a very simple class called Calculator and write a test for it.

Create a file with the name of Calculator.php and copy the code below to the file. This Calculator class only has an Add function. :

class Calculator
{
    public function add($a, $b)
    {
        return $a + $b;
    }
}

Create the test file CalculatorTest.php, and copy the code below to the file. We will explain each function in details.

require 'Calculator.php';
class CalculatorTest extends PHPUnit_Framework_TestCase
{
    private $calculator;
 
    protected function setUp()
    {
        $this->calculator = new Calculator();
    }
 
    protected function tearDown()
    {
        $this->calculator = NULL;
    }
 
    public function testAdd()
    {
        $result = $this->calculator->add(1, 2);
        $this->assertEquals(3, $result);
    }
}
  • Line 2: Includes class file Calculator.php. This is the class that we are going to test against, so make sure you include it.
  • Line 8: setUp() is called before each test runs. Keep in mind that it runs before each test, which means, if you have another test function, it too will run setUp() before.
  • Line 13: Similar to setUp() , tearDown() is called after each test finishes.
  • Line 18: testAdd() is the test function for add function. PHPUnit will recognize all functions prefixed with test as a test function and run them automatically. This function is actually very straightforward: we first call Calculator.add function to calculate the value of 1 plus 2. Then we check to see if it returns the correct value by using PHPUnit function assertEquals.

The last part of the task is to run PHPUnit and make sure it passes all tests. Navigate to the folder where you have created the test file and run the commands below from your terminal:

vendor/bin/phpunit CalculatorTest.php

You should be able to see the successful message as below:

PHPUnit 5.0.9 by Sebastian Bergmann and contributors.
.                           1 / 1 (100%)
 
Time: 40 ms, Memory: 2.50Mb

Data Provider

When to use data provider

When we write a function, we want to make sure it passes a series of edge cases. The same applies to tests. This means we will need to write multiple tests to test the same function using different sets of data. For instance, if we want to test our Calculator class using different data, without data provider, we would have multiple tests as shown below:

require 'Calculator.php';
class CalculatorTest extends PHPUnit_Framework_TestCase
{
    private $calculator;
 
    protected function setUp()
    {
        $this->calculator = new Calculator();
    }
 
    protected function tearDown()
    {
        $this->calculator = NULL;
    }
 
    public function testAdd()
    {
        $result = $this->calculator->add(1, 2);
        $this->assertEquals(3, $result);
    }
 
    public function testAddWithZero()
    {
        $result = $this->calculator->add(0, 0);
        $this->assertEquals(0, $result);
    }
 
    public function testAddWithNegative()
    {
        $result = $this->calculator->add(-1, -1);
        $this->assertEquals(-2, $result);
    }
}

In this case, we can use data provider function in PHPUnit to avoid duplication in our tests.

How to use data provider

A data provider method returns a variety of arrays or an object that implements the Iterator interface. The test method will be called with the contents of the array as its arguments.

Some key points to keep in mind when using data provider are:

  • Data provider method must be public.
  • Data provider returns an array of a collection data.
  • Test method use annotation(@dataProvider) declares its data provider method.

Once we know the key points, it is actually quite straightforward to use data provider. First, we create a new public method, which returns an array of a collection data as arguments of the test method.Then, we add annotation to the test method to tell PHPUnit which method will provide arguments.

Add data provider to our first unit test

Let's modify our tests above using data provider.

require 'Calculator.php';
class CalculatorTest extends PHPUnit_Framework_TestCase
{
    private $calculator;
 
    protected function setUp()
    {
        $this->calculator = new Calculator();
    }
 
    protected function tearDown()
    {
        $this->calculator = NULL;
    }
 
    public function addDataProvider()
    {
        return array(
            array(1,2,3),
            array(0,0,0),
            array(-1,-1,-2),
        );
    }
 
    /**
     * @dataProvider addDataProvider
     */
    public function testAdd($a, $b, $expected)
    {
        $result = $this->calculator->add($a, $b);
        $this->assertEquals($expected, $result);
    }
}
  • Line 18: Add a data provider method. Take note that a data provider method must be declared as public.
  • Line 27: Use annotation to declare the test method's data provider method.

Now, run our test again and it should pass. As you can see, we have utilized data provider to avoid code duplication. Instead of writing three test methods for essentially the same method, we now have only one test method.

Test Double

When to use test double

As mentioned in the first part of this series. One of PHPUnit's powerful features is test double. It is very common in our code that a method of a class calls another class's method. In this case, there is a dependency between these two classes. In particular, the caller class has a dependency on the calling class, but as we already know from part 1, unit test should test the smallest unit of functionality. In this case, it should test only the caller function. To solve this problem, we can use test double to replace the calling class. Since a test double can be configured to return predefined results, we can focus on testing the caller function.

Types of test doubles

Test double is a generic term for objects we use, to replace real production ready objects. In our experience, it is very useful to categorize test doubles by their purpose. It not only makes it easy for us to understand the test case, but also make our code friendly to other parties.

Accordingly to Martin Fowler's post, there are five types of test double:

  • Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.
  • Fake objects actually have working implementations, but usually take some shortcuts, which make them not suitable for production.
  • Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
  • Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent. Mocks are pre-programmed with expectations that form a specification of the calls they are expected to receive. They can throw an exception if they receive a call they don't expect and are checked during verification to ensure they received all the calls they were expecting.

How to create test double

PHPUnit's method getMockBuilder can be used to create any similar user defined objects. Combining with its configurable interface, we can use it to create basically all five types of test doubles.

Add test double to our first unit test

It is meaningless to use test double in our calculator test case, since currently the Calculator class has no dependency on other classes, however, to demonstrate how to use test double in PHPUnit, we will create a stub Calculator class and test it.

Let's add a test case called testWithStub to our existing class:

public function testWithStub()
{
    // Create a stub for the Calculator class.
    $calculator = $this->getMockBuilder('Calculator')
                       ->getMock();
 
    // Configure the stub.
    $calculator->expects($this->any())
               ->method('add')
               ->will($this->returnValue(6));
 
    $this->assertEquals(6, $calculator->add(100,100));
}
  • getMockBuilder() method creates a stub similar to our Calculator object.
  • getMock() method returns the object.
  • expects() method tells the stub to be called any number of times.
  • method() method specifies which method it will be called.
  • will() method configures the return value of the stub.

We have introduced some basic usage of PHPUnit, which provides almost all the features we would need to create unit tests. You should always try to find more information from its official manual as you needed.

TDD by example

In this section, we will demonstrate the process behind TDD through a very simple example. You should concentrate on how the three phases of TDD are carried out in this example.

Suppose we are given a task of building a price calculator for our e-commerce system. The class we are going to develop will be PriceCalculator. Let's first setup the project's folder and file structure as well as its dependencies.

As usual, we will use Composer as our package manager and PSR-4 as our code standard. The only third party dependency is PHPUnit. To set things up, we will create a folder src for placing our source files, and a folder tests for placing test files. We will also create src/PriceCalculator.php and tests/PriceCalculatorTest.php respectively. Finally, we will create a composer.json file as below:

{
    "require": {
        "phpunit/phpunit": "^5.0"
    },
    "autoload": {
        "psr-4": {
            "Dilab\\Order\\": "src"
        }
    }
}

This file tells Composer to download PHPUnit and tells autoloader that our source code follows PRS-4 standard.

By running command composer install, we should end up with a folder structure as below:

.
+-- src
|   +-- PriceCalculator.php
+-- tests
|   +-- PriceCalculatorTest.php
+-- vendor
|   +-- dependency-1
|   +-- dependency-2
|   +-- dependency-3
|   +-- dependency-xxx
+-- composer.json
+-- composer.lock

The final piece we need for the setup is a phpunit.xml file to config PHPUnit. Let's create it as the folder root.

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false"
         syntaxCheck="false"<
    <testsuites>
        <testsuite name="Test Suite">
            <directory suffix=".php">./tests/</directory>
        </testsuite>
    </testsuites>
</phpunit>

Our final folder structure should be as shown below:

+-- src
|   +-- PriceCalculator.php
+-- tests
|   +-- PriceCalculatorTest.php
+-- vendor
|   +-- dependency-1
|   +-- dependency-2
|   +-- dependency-3
|   +-- dependency-xxx
+-- composer.json
+-- composer.lock
+-- phpunit.xml

Red phase

At this phase we will plan how our API will look like and create failing test. In this example, the required API method is very simple. We just want a method that accepts an array as its parameter and calculate the total price. We will name this method total .

Let's create some tests in tests/PriceCalculatorTest.php file before we write any source code.

namespace Dilab\Order\Test;
use Dilab\Order\PriceCalculator;
 
class PriceCalculatorTest extends \PHPUnit_Framework_TestCase
{
    private $PriceCalculator;
 
    public function setUp()
    {
        parent::setUp();
        $this->PriceCalculator = new PriceCalculator();
    }
 
    public function tearDown()
    {
        parent::tearDown();
        unset($this->PriceCalculator);
    }
 
    /**
    * @test
    */
    public function object_can_created()
    {
        $priceCalculator = new PriceCalculator();
        $this->assertInstanceOf('Dilab\Order\PriceCalculator', $priceCalculator);
    }
 
    /**
    * @test
    */
    public function should_sum_price()
    {
        $items = [
            ['price' => 100],
            ['price' => 200],
        ];
 
        $result = $this->PriceCalculator->total($items);
        $this->assertEquals(300, $result);
    }
 
    /**
    * @test
    */
    public function empty_items_should_return_zero()
    {
        $items = [];
        $result = $this->PriceCalculator->total($items);
        $this->assertEquals(0, $result);
    }
}

We have created three tests for PriceCalculator:

  • public function object_can_created() : This test assures the object can be instantiated. Some may argue that this is unnecessary, but from a TDD point of view, we like to have such a simple test. When this test is passed, we can naturally move on to ones testing its real behaviour.
  • public function should_sum_price() : This method tests whether total method does its job as described.
  • public function empty_items_should_return_zero() : This method tests an edge case, where there is no item in the order. In such case, total method should return zero.

Now if we run vendor/bin/phpunit from terminal, we should get error as expected as below:

Fatal error: Class 'Dilab\Order\PriceCalculator' not found in tests/PriceCalculatorTest.php

Green phase

The task of this phase is to make the failing tests above pass with the easiest but not necessarily the best code. The ultimate goal of this phase is the green message.

The implementation is fairly easy. All we need to do is to sum up the value with a foreach loop.

namespace Dilab\Order;
class PriceCalculator
{
    public function total($items)
    {
        $total = 0;
        foreach ($items as $item) {
            $total += $item['price'];
        }
        return $total;
    }
}

Now if we run vendor/bin/phpunit from terminal, we should get a green message as below:

PHPUnit 5.09 by Sebastian Bergmann and contributors.
 
...             3 / 3 (100%)
 
Time: 78 ms, Memory: 2.75Mb

Refactoring phase

This is the final phase of TDD, which we believe is the most valuable part of TDD. In this phase, we will take a look at the code we have written previously, and think of ways to make it cleaner and better.

We are using a foreach loop inside total method. It loops through $items array and returns the sum of the each individual element. This is actually a perfect use case of array_reduce method. Function array_reduce iteratively reduces the array to a single value using a callback function. Let's refactor our code by replacing foreach loop with array_reduce .

public function total($items)
{
    return array_reduce($items, function ($carry, $item) {
       return $carry + $item['price'];
    }, 0);
}

If we run our tests again and they all still pass, we are good to go. Because we need to run the tests constantly to make sure refactoring does not break anything, it is important to keep our code fast.

We have cleaned up our code from five lines to two lines. There is no more temporary variable. The method has become easier to debug. There might not be apparent benefits for doing so in this example, but imagine this in a large scale project, even cleaning up one line of code could potentially make development easier.

This is the end of TDD. To emphasize again, the spirit of TDD is to let tests drive our development. Using PHPUnit in a project does not necessarily make it a TDD driven project. It is the three phases processes involved in the development that make it TDD.

The end

Hopefully this simple tutorial helped you with your development. If you like our post, please follow us on Twitter and help spread the word. We need your support to continue. If you have questions or find our mistakes in above tutorial, do leave a comment below to let us know.