Mastering PHPUnit: an introduction

May 3, 2024ยท
Gert de Pagter
Gert de Pagter
ยท 5 min read

In this blog series we’ll go from writing our first unit test to being a PHPUnit master. This first post will go over the basics, and introduce you to PHPUnit

What is PHPUnit?

PHPUnit is the de facto testing framework for PHP. It initially started in 2001, over 23 years ago at this point. And even today it is still under active development. All its development is done on GitHub, where you can open issues if you have a feature request, or find a bug.

Why do we test?

There is a lot of talk about whether you should even write unit tests. To summarize my thoughts on the matter: You should. I don’t trust myself (or anyone else) to write perfect code. Which is why we need to validate that our code works. Now we could manually verify every bit of code we write. For web development we could reload a page, click a few buttons, and consider our code working. But, for every change we make we’ll have to do that work again.

With unit testing we validate this automatically. We write a test that checks that our code does what we think it does. Now instead of having to perform all the possible actions of our code every time we refactor, we simply run our unit tests, and we know in a matter of milliseconds if our code works.

The ‘downside’ of unit testing is that we have to write these tests, which means we have to write more code. It also means that on top of code we need to maintain our unit tests as well. Which means dependency updates, refactors etc. In my opinion the investment in unit tests outweighs the costs.

AAA: The three steps to every unit test

In general, we can split up our tests in three parts: arrange, act, assert.

Arrange

In the arrange phase we do all the set up to make sure that we can run the code that is needed.

We may need to set up some mocks (more about those in a future post), set up the environment or do other things.

Act

In the act phase we run the actual code that we want to execute. In unit testing this is usually just one line of code, where we run the code.

Assert

After we run the code that we want to test, we of course need to check that the code did what we want to do. So we assert that it did what we expected it to do. Here we’ll check either the output of the method test, or check for the side effects that the method caused.

Adding a test to a project

Let’s assume we have a project with the following structure. For this i’ll assume you’ve worked with composer before and have autoloading etc. set up.

๐Ÿ“ฆ project
โ”ฃ ๐Ÿ“‚ src
โ”ƒ โ”— ๐Ÿ“œ Calculator.php
โ”ฃ ๐Ÿ“œ composer.json

With the Calculator.php looking like this:

<?php

declare(strict_types=1);

namespace App;

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

First off we’ll run composer require --dev phpunit/phpunit, to install phpunit as a dev dependency.

Then we’ll create a file called phpunit.xml to configure phpunit with. (We’ll go into detail about this config file in a future post.)

<?xml version="1.0" encoding="UTF-8"?>
<phpunit
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
>
    <testsuites>
        <testsuite name="AllTests">
            <directory>test/</directory>
        </testsuite>
    </testsuites>
</phpunit>

Then we’ll continue by creating a file called CalculatorTest.php in the test folder we’ll create.

So now our project will look like this:

๐Ÿ“ฆ project
โ”ฃ ๐Ÿ“‚ src
โ”ƒ โ”— ๐Ÿ“œ Calculator.php
โ”ฃ ๐Ÿ“‚ test
โ”ƒ โ”— ๐Ÿ“œ CalculatorTest.php
โ”ฃ ๐Ÿ“œ composer.json
โ”ฃ ๐Ÿ“œ phpunit.xml

Now we’ll write our first test. For now just an empty test, which doesn’t do anything.

<?php

declare(strict_types=1);

namespace App\Test;

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

class CalculatorTest extends TestCase
{
    public function testAdd(): void
    {
    }
}

If we now run phpunit, we’ll get an output like this:

$ vendor/bin/phpunit 
PHPUnit 11.1.3 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.3.6
Configuration: /Volumes/SourceCode/Private/hugo-blog-test/phpu/phpunit.xml

R                                                                   1 / 1 (100%)

Time: 00:00.007, Memory: 6.00 MB

There was 1 risky test:

1) App\Test\CalculatorTest::testAdd
This test did not perform any assertions

/Volumes/SourceCode/Private/hugo-blog-test/phpu/test/CalculatorTest.php:12

OK, but there were issues!
Tests: 1, Assertions: 0, Risky: 1.

PHPunit has executed our test, but it didn’t do any assertions, which is a problem.

So let actually test something, and fill in the testAdd method, like so:

public function testAdd(): void
{
    // arrange
    $calculator = new Calculator();

    //act
    $output = $calculator->add(2, 5);

    //assert
    self::assertSame(7, $output);
}

As you can see we have the three steps, arrange, act, assert. Now in normal development you wouldn’t explicitly add these comments of the different phases, but it’s good to keep in mind how you structure your test.

Now if we run phpunit we’ll get the following output:

$ vendor/bin/phpunit
PHPUnit 11.1.3 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.3.6
Configuration: /Volumes/SourceCode/Private/hugo-blog-test/phpu/phpunit.xml

.                                                                   1 / 1 (100%)

Time: 00:00.003, Memory: 6.00 MB

OK (1 test, 1 assertion)

That’s a success! We have a test, and we made an assertion about our code, now if we update the calculator class we can run the test again, and validate our code still works as before.

Next week we’ll go over how to determine what to test, how to test it, and some common problems we can face while testing. If you want to get notified of the next blog post, join the newsletter.