April 5, 2021

Unit Testing With PHPUnit

Reading time about 8 min

What is Unit Testing and PHPUnit?

Unit testing is a way of testing a unit, the smallest piece of code, that can be logically isolated in a system. These units are tested against expected results.

In object-oriented programming, a unit is often an entire interface, such as a class, but could be an individual method. In procedural programming, a unit may be an individual program, function, procedure, etc. Unit tests written by the developer ensure that the source code meets the requirement and behaves in an expected manner.

PHPUnit is a programmer-oriented testing framework for PHP.

Why Unit Testing?

  • Reduces Defects in the newly developed features or reduces bugs when changing the existing functionality.
  • Improves design and allows better refactoring of code.
  • Helps capture defects in a very early phase consequently reducing testing costs.
  • Unit Tests provide living documentation of an application. Developers wanting to learn what functionality is provided by a particular unit can refer to the Unit Tests to gain understanding.

That’s why Sendinblue developers write unit tests and run these tests for every pull request and branch push using Travis CI.

Conventions For Writing Unit Tests

Below are major conventions to write tests with PHPUnit.

  1. The name of the test file should be *Test.php, e.g. for file Product.php test file should be ProductTest.php
  2. Test class (ProductTest) inherits from PHPUnit\Framework\TestCase.
  3. Tests are the methods that should be named as test*, for e.g. testGetName(). Alternatively, you can use the @test annotation in a method’s DocBlock to mark it as a test method.
  4. Test methods are `public`.
  5. Mirror your src/ the directory structure in your tests/ folder structure.
| -- src
      | -- Controller
      | -- Entity
            | -- Comment.php
            | -- Post.php
            | -- Tag.php
      | -- Repository

| -- tests
      | -- Controller
      | -- Entity
            | -- CommentTest.php
            | -- PostTest.php
            | -- TagTest.php
      | -- Repository

NOTE: Above src directory structure is from the Symfony project. You may have a completely different code & directory structure.

Installing PHPUnit

One can easily install PHPUnit via Composer:-

composer require --dev phpunit/phpunit

We’re using the --dev flag as we only need this installed for our dev environment, as we don’t want to run these tests in production.

Let’s Get Started

Once you have installed the PHPUnit, create a phpunit.xmlfile. This is where you configure the specific options for your tests.

<?xml version="1.0" encoding="UTF-8"?>

<phpunit bootstrap = "vendor/autoload.php" colors = "true">

    <testsuites>
        <testsuite name="Project Test Suite">
            <directory>tests</directory>
        </testsuite>
    </testsuites>

</phpunit>
  • The bootstrap attribute describes the script to load before running the test.
  • colors attribute is responsible to decide whether the output of PHPUnit will be colorful or not.

Let’s now create directory tests where we will write the tests. After this, our project will look like

| -- src
| -- tests
| -- composer.json
| -- composer.lock
| -- index.php
| -- phpunit.xml

Go to the src directory and create a class Calculator:

<?php

namespace App;

class Calculator
{
    public function add(int $firstNumber, int $secondNumber): int
    {
        return $firstNumber + $secondNumber;
    }
}

Go to the tests directory and create your first test class CalculatorTest.php to test Calculator add method:


<?php

namespace Tests;

use PHPUnit\Framework\TestCase;
use App\Calculator;

class CalculatorTest extends TestCase
{
    public function testAdd(): void
    {
        $calculator = new Calculator();
        $expected = 15;
        $actual = $calculator->add(5, 10);
        $this->assertEquals($expected, $actual);
    }
}

In the above test, on linenumber #15 we have used assertEquals to validate if expected and actual values are equal. There are a lot of such assertions available to make our life easier.

Let’s run the test, in terminal go to your project directory and run ./vendor/bin/PHPUnit

Congratulations you have just created and executed your first test.

Data Providers

While writing the tests we want to cover as much as possible, so needs to validate our functionality against a different set of data. We are luck Data Providers.

Rules To Use Data Providers:

  • The data provider method returns either an array of arrays or an object that implements the Iterator interface and yields an array for each iteration step.
  • Data provider method must be public
  • The test method uses annotation(@dataProvider) to declare its data provider method.

Let’s modify our first test with Data Providers

<?php

namespace Tests;

use PHPUnit\Framework\TestCase;
use App\Calculator;

class CalculatorTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public  function testAdd($firstNumber, $secondNumber, $expected): void
    {
        $calculator = new Calculator();
        $actual = $calculator->add($firstNumber, $secondNumber);
        $this->assertEquals($expected, $actual);
    }

    public function additionProvider()
    {
        return [
            [0, 0, 0],
            [0, 5, 5],
            [7, -2, 5],
            [10.8, 10.1, 20.9]
        ];
    }
}

Let’s run the tests again.

OOPS, one test failed for the data set [10.8, 10.1, 20.9]. Here we were expecting that the add method of class Calculator should return 20.9 for passed parameters 10.8 & 10.1. However, it has returned 20 instead of 20.9
It’s evident there is something wrong with the add method.
Aha, found the issue was happening because we are using int for method parameters & return type declarations, let’s modify the add method of the Calculator class to use float instead of int.

<?php

namespace App;

class Calculator
{
    public function add(float $firstNumber, float $secondNumber): float
    {
        return $firstNumber + $secondNumber;
    }
}

Try again running the tests. Hurray, all tests pass.

Code Coverage Analysis

In computer science, code coverage is a measure used to describe the degree to which the source code of a program is tested by a particular test suite. A program with high code coverage has been more thoroughly tested and and as a result will have a lower chance of containing software bugs.

PHPUnit provides below software metrics for code coverage analysis:

  • Line Coverage: Measures whether each executable line was executed.
  • Function and Method Coverage: Measures whether each function or method has been invoked and all executable lines of function or method are covered.
  • Class and Trait Coverage: Measures whether each method of a class or trait is covered.
  • Change Risk Anti-Patterns (CRAP) Index: The calculation is based on the cyclomatic complexity and code coverage of a unit of code. Code that is not too complex and has adequate test coverage will have a low CRAP index.

In order to check the code coverage of your code, you can simply run the below command:

.vendor/bin/phpunit --coverage-text

Here you can find the multiple options available for code coverage analysis The Command-Line Test Runner.

NOTE: In order to use the code coverage option, we must have Xdebug installed and enabled in our system.

Code Coverage Metrics

Enough of the theory about code coverage analysis, as an illustration, let me show you what metrics actually look like. The below screenshot is showing code coverage for one of our GitHub repositories.

Here at Sendinblue we follow OOPS. From a code coverage point of view, we mainly focus on Class & Method metrics and try to cover 70% of the code.
We should also keep an eye on how many assertions we have for a test. Ideally, one test should have one assertion but this may lead us toward writing too many test methods. So, it’s ok to have multiple assertions for a single test. Just make sure our test methods don’t try to test too many different things at once.

Final Words

In this article, we saw how we can write unit tests using PHPUnit. There are a lot more other important topics like FixturesTest Doubles, etc.
Unit testing is excellent and helps us to have a stable code with a lesser number of issues however, it has limitations too. As unit testing only covers isolated testing of the small units of the code, we can’t be sure how these small units of code will behave once they integrate with each other. Therefore, you also need to run integration tests that test individual units/components together as a group.

“No amount of testing can prove a software right, a single test can prove a software wrong.” — Amir Ghahrai

References