PHP test driven development

Test Driven Development (or TDD) in PHP introduction

TDD is a software development process in which a failing test is written before any other production code. It is a test first methodology and is often a part of agile development methodology.

TDD consists of 3 main rules:

  1. No code can be written before the test exists.
  2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
  3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

To explain it in a simple way, first we need to write a small minimal test, for the code only to fail, and then we need to write as little functionality code as possible to pass the failing unit test.

Let's choose the tools:

Let's start by installing some tools to begin with TDD. 

There are many PHP testing frameworks and tools, PHPUnit and Codedeception being one of the most popular. This time I'll choose Codeception, because it is based on PHPUnit, so it basically contains all of the things needed for unit testing, but it also provides capability to make functional and acceptance tests.

First, let's install Codeception via composer:

composer require "codeception/codeception" --dev

Then we need to run:

php vendor/bin/codecept bootstrap

This will create configuration file for codeception, codeception.yml, tests directory and default test suites.

Application to create

For simplicity reasons we're going to create a simple calculator app, because it doesn't require many dependencies and no object mocking is needed.

Our calculator app will be able to do simple mathematical operations: (+, -, *, / ) and square root operation.

Now we can generate actual class for our Calculator app by running the following command:

php vendor/bin/codecept generate:test unit Calculator

If everything's OK you should get a response like this:

Test was created in /PROJECT_PATH/tests/unit/CalculatorTest.php

Iteration steps

Now that we know what app we want to develop, let's see what specific steps should we follow to develop in TDD mode:

  1. Write a failing test.
  2. Run it, and see it fail.
  3. Write the code to cover the failing test.
  4. Run the test again and see it pass.
  5. Repeat Step 1

Now if we open CalculatorTest.php you'll see something like this:

class CalculatorTest extends \Codeception\Test\Unit
{
    /**
     * @var \UnitTester
     */
    protected $tester;
    
    protected function _before()
    {
    }

    protected function _after()
    {
    }

    // tests
    public function testSomeFeature()
    {

    }
}

Let's make some changes to this class and follow these steps.

First iteration

First feature of any calculator app is addition, so let's work on that.

We can rename testSomeFeature() to testAdd() and add some simple call to our Calculator app:

public function testAdd()
{
    $calculator = new Calculator();
}

I intentionally added only 1 line and haven't even attempted to call $calculator->add($a, $b), because that would break the 2nd rule of TDD, which is: "You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures."

Now if we try to run unit tests by calling the following command:

php vendor/bin/codecept run unit

We will get error response similar to this:

Unit Tests (1) ----------------------------------------------------------------------------------------------------------------------------------------------
E CalculatorTest: Add (0.01s)
-------------------------------------------------------------------------------------------------------------------------------------------------------------


Time: 89 ms, Memory: 10.00 MB

There was 1 error:

---------
1) CalculatorTest: Add
 Test  tests/unit/CalculatorTest.php:testAdd
                                        
  [Error] Class 'Calculator' not found  
                                        
#1  /PROJECT_PATH/tests/unit/CalculatorTest.php:20

ERRORS!
Tests: 1, Assertions: 0, Errors: 1.

 We can clearly see that Calculator class is missing, so let's go to the 2nd iteration step and make this test pass:

<?php

namespace App\Calculator;

class Calculator
{
}

Although doesn't do anything functional, if we try to run unit tests again, we will see different result:

Unit Tests (1) ----------------------------------------------------------------------------------------------------------------------------------------------
✔ CalculatorTest: Add (0.00s)
-------------------------------------------------------------------------------------------------------------------------------------------------------------


Time: 60 ms, Memory: 10.00 MB

OK (1 test, 0 assertions)

Congratulations, unit tests have passed!

Even though Calculator app so far is not useful, we've completed all the steps of first iteration.

Let's begin second iteration by creating testAdd() method and some code to it:

public function testAdd()
{
    $calculator = new Calculator();

    $result = $calculator->add(2, 3);

    $this->assertEquals(5, $result);
}

Now we need to run test one more and this time we get a different error:

There was 1 error:

---------
1) CalculatorTest: Add
 Test  tests/unit/CalculatorTest.php:testAdd
                                                                     
  [Error] Call to undefined method App\Calculator\Calculator::add()  
                                                                     
#1  /PROJECT_PATH/tests/unit/CalculatorTest.php:25

ERRORS!
Tests: 1, Assertions: 0, Errors: 1.

So now we need to create Calculator::add() method and necessary code to pass our test:

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

After this if we need to run the tests once again to see if that has fixed the error:

Unit Tests (1) ----------------------------------------------------------------------------------------------------------------------------------------------
✔ CalculatorTest: Add (0.00s)
-------------------------------------------------------------------------------------------------------------------------------------------------------------


Time: 64 ms, Memory: 10.00 MB

OK (1 test, 1 assertion)

Ad you can see, everything seems to be working just fine, because Calculator::add($a, $b) returned correct result, so mathematical addition works.

From now on, we can continue iterating multiple times in order to add additional features to our calculator(-, *, /, .etc.), but the principle stays the same: write failing test, fix it by adding some working code, test again to see tests succeed and repeat.

TDD means different approach to coding

As we can now understand, TDD approach doesn't only mean we start from test, it also means different thinking about how we approach creating features. With TDD we tend to focus more on how should things work from user perspective, and what features user needs instead of focusing on just  making our code fancy and super feature rich. 

TDD means code stays less vulnerable to changes

I cannot count how many times I've heard developers saying: "When we started this project, we tried to cover up to 95% of our code with tests, but now as we need to move much more fast and we don't have enough time, we abandoned the tests with hopes that some day we will be able to return writing them".

Doesn't this sound familiar to you ? If it does, you'll understand one of the main TDD advantages right away, but if it doesn't, I'll try to add some more in favour of using TDD.

Working in TDD regime means that your code will be always covered with tests. Any feature that you want to add or update, will first be covered by test before even any code changes to that feature. It means that in theory this code is super stable. 

Summary

In this article I've covered some introduction on how to approach TDD with PHP and Codeception. It's quite easy to start, but appropriate effort is needed in order to master this thing fully.