DEV Community

Cover image for TDD with PestPHP
Roberto B.
Roberto B.

Posted on • Updated on

TDD with PestPHP

Test Driven Development (TDD) is a software development practice.

The typical flow is:

  • writing the test cases starting from the requirements;
  • writing the code that solves the tests;
  • refactoring the code to pass all tests.

Writing tests before the code, allows the developer to:

  • focus on requirements;
  • see if some requirements are missing or not detailed;
  • focus about the edge cases (empty params, invalid params etc). This is very useful for thinking outside the "happy path".

This approach helps and supports the developer for creating a "better and more maintainable code" because it forces the developer making the code/functions testable from the beginning:

  • splitting code in smaller and easier "pieces";
  • favoring the single responsibility approach;
  • favoring the code isolation (or encapsulation).

But, yes, fewer words and more code.

For our example, we are going to create a class with a static function for calculating the mean (the average).

Create your project from scratch

Create a new directory and jump into the new directory:

mkdir tdd
cd tdd
Enter fullscreen mode Exit fullscreen mode

Install PestPHP

Now you can install the testing framework PestPHP.
PestPHP is built on top of the mature PHPUnit, and it aims to provide a nice and smooth interface for the developer that want to write tests.

composer require pestphp/pest --dev --with-all-dependencies
mkdir tests
./vendor/bin/pest --init
Enter fullscreen mode Exit fullscreen mode

I'm going to skip the right configuration for PSR-4 autoloading, so I will create a file and include that in the test file via require

Create Stat class

Create your "stat.php" file with the class "Stat":

<?php

class Stat
{
}
Enter fullscreen mode Exit fullscreen mode

Yes, I know, the class is empty. Before to write the method and the implementation, you need to create the new test file.
Create "StatTest.php" file in the "tests" directory. The convention to write tests in "tests" directory and naming the file with "Test.php" suffix, allows PestPHP to automatically find the right tests.

Create the test file

File "tests/StatTest.php":

<?php
require('./stat.php');

test('test mean', function () {
    expect(Stat::mean([1,2,3]))->toEqual(2);
});
Enter fullscreen mode Exit fullscreen mode

Before implementing the "mean()" method, we are expecting (expect()) that calling "Stat::mean()" with an array [1,2,3] as input parameter, it returns a value equal "2".

This is the happy path.

But I want also to think about "bad scenarios" for example if the user will call the method "Stat::mean()" with an empty array, I expect to retrieve a "null" value

test('test mean with empty data', function () {
    expect(Stat::mean([]))->toBeNull();
});
Enter fullscreen mode Exit fullscreen mode

Calling "Stat::mean()" with a non array parameter (for example an integer) I expect to have an exception:

test('test mean with not array', function () {
    expect(
        fn () => Stat::mean(42)
    )->toThrow(TypeError::class);

});
Enter fullscreen mode Exit fullscreen mode

If you run the tests:

./vendor/bin/pest
Enter fullscreen mode Exit fullscreen mode

You will receive errors like "undefined method" because the class Stat is still empty, and it doesn't implement any methods.

Call to undefined method Stat::mean()
Enter fullscreen mode Exit fullscreen mode

So now, jump into "stat.php" file and implement the method mean() that for now will always return the value 0.0.

<?php

class Stat
{
    public static function mean(array $data): ?float
    {
        return 0.0;
    }
}
Enter fullscreen mode Exit fullscreen mode

If you run the test now, you will still receive errors.

Image description

The first one is about the assertion that expect to receive 2 as value. The current implementation is returning 0.0:

  • Tests\StatTest > test mean() with array parameter
  Failed asserting that 0.0 matches expected 2.
Enter fullscreen mode Exit fullscreen mode

The second one is about the assertion that expect to receive "null" if the input parameter is an empty array. The current implementation is returning 0.0:

  • Tests\StatTest > test mean() with empty array parameter
  Failed asserting that 0.0 is null.
Enter fullscreen mode Exit fullscreen mode

The test are failing, so now you need to focus on your implementation, on your code and implement the logic and making the test "green".

In your "stat.php" file in the "mean()" method, try to implement the mean:

  • count the elements in the array;
  • calculate the sum of the elements of the array;
  • divide the sum with the count.
<?php

class Stat
{
    public static function mean(array $data): ?float
    {
        $count = count($data);
        $sum = array_sum($data);
        return $sum / $count;
    }
}
Enter fullscreen mode Exit fullscreen mode

If you execute the tests, the only one that fails is the test that calls "mean()" method with empty array and raises a "Division by zero exception".

Image description

"Division by zero" is raised because the count is equal to 0. So, you need to check the length of the array, and if it is 0, you need to return "null".

<?php

class Stat
{
    public static function mean(array $data): ?float
    {
        $count = count($data);
        if ( ! $count) {
            return null;
        }
        $sum = array_sum($data);
        return $sum / $count;
    }
}
Enter fullscreen mode Exit fullscreen mode

If you execute the tests ...

Image description

All GREEN for us!

So, we:

  • Wrote the test focusing on happy path and "bad scenarios";
  • Wrote the code iteratively until all test passes.

And you, are you applying TDD practice in your development process?

Latest comments (1)

Collapse
 
wanderleyfa profile image
Wanderley

great text, it demonstrates exactly what is needed to choose PestPHP as a testing framework for my project. the only thing i missed was a section talking about choosing what to test and what part not to test, and yes i know this is a complicated and controversial subject.