TDD in practice with PHPUnit

TDD has become an essential practice for developers to have more confidence and engage practices such as refactoring. For example, if you updated some critical library in our application, you can run all tests to see if some implementation has been broken.

According to wikipedia:

Test-driven development (TDD) is a software development process that relies on the repetition of a very short development cycle: first the developer writes an (initially failing) automated test case that defines a desired improvement or new function, then produces the minimum amount of code to pass that test, and finally refactors the new code to acceptable standards. Kent Beck, who is credited with having developed or ‘rediscovered’ the technique, stated in 2003 that TDD encourages simple designs and inspires confidence.

This tutorial aimed to drive the process of TDD in practice, demonstrating the following topics:

  • The basic concept about RED-GREEN-REFACTOR cycle;
  • Setup our testing tools (in this case PHPUnit);
  • Basic structure of a Test classes;
  • Creating some concrete features, applying TDD.

Basic concept of TDD

I will explain in-short, what you should know about TDD to put in practice.

TDD has a concept called RED-GREEN-REFACTOR.

RED phase:

  • Write the test of a new feature or a bug to be fixed;
  • Run the tests. The new one are going to fail.

GREEN phase:

  • Write the code needed to match the test’s expectations.
  • Run the tests. Now the newly written test should be passing.

REFACTOR phase:

  • Refactor the code written to become more well written, abstract repetition, DRY it, etc.

You will see that this cycle will performed several times.

Let’s create the basic structure for our application.

Creating the Folder structure

First at all, let’s create the following folder structure:

Installing Composer

Before getting started, install Composer.

$ curl -sS https://getcomposer.org/installer | php

Inside the root’s project folder, create the composer.json.

{
    "name": "guilhermeguitte/tdd-first-steps",
    "description": "A simple introduction explain how to apply TDD with PHPUnit.",
    "license": "MIT",
    "authors": [
        {
            "name": "Guilherme Guitte",
            "email": "guilherme.guitte@gmail.com"
        }
    ],
    "minimum-stability": "stable",
    "require-dev": {
    }
}

Installing PHPUnit

Now, add at require-dev the phpunit dependency.

"require-dev": {
    "phpunit/phpunit": "4.2.*"
}

Run in console composer install to add all dependencies:

$ composer.phar install --dev

Create the file phpunit.xml in root directory. PHPUnit need to know where our tests files are located and which configuration to use.

<phpunit
         backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
>
   <testsuites>
     <testsuite>
       <directory>tests/</directory>
     </testsuite>
   </testsuites>
</phpunit>

The final folder structure should be equals to:

Test’s file structure

See the example of Test class below:

<?php

class BasicExampleTest extends PHPUnit_Framework_TestCase
{
    public function testSomeThing() {}
    public function nonTest() {}
}

The class definition above, is the way we should create our Test classes. We have to extend PHPUnit_Framework_TestCase to be able to use the set of tools that PHPUnit provides.

Only the methods that have the test prefix will be called by PHPUnit. So the method testSomeThing() will be called.

Now running the vendor/bin/phpunit command:

$ vendor/bin/phpunit

We will see:

At this point, we have did the setup of our application with PHPUnit, now we can create something concrete.

Commit it

Please, commit this changes:

$ git init
$ git add -A .
$ git commit -m 'basic structure'

The Feature’s scope

Until now, we’ve seen a basic structure in order to apply TDD, at this moment we will create more complex tests. The next example, will be based on Product class. This class will contains the name, price variables and 2 methods:

1) `isValid`: Verifies the state of itself is valid.
2) `getPriceWithCurrency`: Renders the price with currency.

Validating Product’s name

Create the ProductTest.php at test’s folder.

class ProductTest extends PHPUnit_Framework_TestCase
{
}

The first step is make a test to verify if the name attributes is really string.

public function testShouldValidateNameIsString()
{
    $product       = new Product;
    $product->name = 123123;

    $this->assertFalse(
        $product->isValid(),
        'The name attributes is not a string'
    );
}

Run the phpunit at console:

$ vendor/bin/phpunit

The error: Fatal error: Class 'Product' not found, was caused by non-existency Product class.

Create the class Product inside src/ folder.

<?php

class Product
{
    public $name;
    public $price;
}

Add to composer.json after the require-dev key to map the classes inside src/:

"autoload": {
    "classmap": [
        "src"
    ]
}

Let’s run composer.phar dumpautoload to map all classes created.

$ composer.phar dumpautoload

Let’s run again the phpunit:

$ vendor/bin/phpunit

Hmm… has a new kind of error (RED Phase):

This errors was caused by the method isValid was not implementing the functionality to validate the name attribute.

Now, we will implement the isValid() method:

public function isValid()
{
    if(! is_string($this->name)) {
        return false;
    }
}

Run again all tests (GREEN Phase):

$ vendor/bin/phpunit

Now let’s say if the name is empty, this test will cover? No, will not. To be sure, let’s create the following test:

public function testShouldValidateNameISNotEmpty()
{
    $product       = new Product;
    $product->name = '';

    $this->assertFalse($product->isValid());
}

Run the all tests:

$ vendor/bin/phpunit

As you can see the testing was failing, and now we should implement the following code.

public function isValid()
{
    if(! $this->name || ! is_string($this->name)) {
        return false;
    }
}

Running all tests again:

$ vendor/bin/phpunit

Let’s refactor

Looking the ProductTest.php, you can note a little repetition in the code when you are testing the name attribute when is empty or a valid string, we can you can use a tool called DataProvider at PHPUnit.

Let’s create a new function called ‘invalidNames’ in ProductTest.php:

public function invalidNames()
{
    return[
        [''],
        [123],
        [null]
    ];
}

Now you can remove all others method to validate ‘name’ attributes, and implements the following code:

/**
 * @dataProvider invalidNames
 */
public function testShouldValidateNameAttribute($value)
{
    $product       = new Product;
    $product->name = $value;

    $this->assertFalse($product->isValid());
}

A Data Provider, is a tool to remove data duplication when are you testing some repetitive values. To use this feature you need create a public method inside test class, returning all possible values. The method’s name should be write in a comment on the function with @dataProvider. The values returned by the function will be passed as argument at the test. At the example above, PHPUnit will run the test case function 3 times passing the returned values.

So far, we have created a validation of the name attribute, testing and refactor this method using @dataProvider tool.

Running all tests, should be green.

Validating Product’s price

Now we will implement the next test case to price attributes, using @dataProvider.

/**
 * @dataProvider invalidPrices
 */
public function testShouldValidatePriceAttribute($value)
{
    $product        = new Product;
    $product->name  = 'Drill';
    $product->price = $value;

    $this->assertFalse($product->isValid());
}

public function invalidPrices()
{
    return[
        [''],
        ['123123'],
        [null],
        ['some string']
    ];
}

Running the tests in terminal $ vendor\bin\phpunit you can see:

Now let’s implement to make pass this tests.

public function isValid()
{
    if(! $this->name || ! is_string($this->name)) {
        return false;
    }

    if(! $this->price || ! $this->hasPriceValid()) {
        return false;
    }

    return true;
}

protected function hasPriceValid()
{
    return is_float($this->price) || is_integer($this->price);
}

Running the tests in terminal $ vendor\bin\phpunit all is green.

Please, commit this changes:

$ git add -A .
$ git commit -m 'Basic validation name and price fields'

Creating getPriceWithCurrency() method

public function testShouldReturnPriceWithCurrency()
{
    $product        = new Product;
    $product->price = 10;
    $expected       = "$ 10";

    $this->assertEquals($expected, $product->getPriceWithCurrency());
}

public function getPriceWithCurrency()
{
    return "$ " . $this->price;
}

Running all tests with “vendor/bin/phpunit”:

Commit it

Please, commit this changes:

$ git add -A .
$ git commit -m 'new getPriceWithCurrency() method has been created in order to return the price with currency as string'

Conclusion

The TDD has a lot of benefits:

  • Give confidences to refactor you code.
  • Creates a layer to knows if was a regression of our code.
  • If the feature is difficult to test, it’s can mean a bad code design.

The process of TDD is something that you will incorporate in our day-to-day.

I’ve heard people say: - “TDD is complicated, I not know how to start”. - “I don`t know how to use TDD with my framework X”.

I will explain how you can do this:

- First create a branch.
- Start creating our first test.

The same way you learn to code, coding, this is valid for learning tests, test something.

Links

http://phpunit.de/manual/current/en/index.html

http://phpunit.de/manual/current/en/appendixes.assertions.html

http://www.amazon.com/Growing-Object-Oriented-Software-Guided-Tests/dp/0321503627/ref=sr_1_1?ie=UTF8&qid=undefined&sr=8-1&keywords=growing+object+oriented+software+guided+by+tests

https://leanpub.com/laravel-testing-decoded

Guilherme Guitte

Read more posts by this author.

São Paulo, Brasil http://www.guitte.org