A Moderately Short PHP Tutorial

Testing

If we want to work on even a moderately complex program over time, we need automated testing - manually testing everything every time we make a change would quickly become unsustainable.

Installing PHPUnit

The leading test framework for PHP is Sebastian Bergmann's PHPUnit.

Since we already have composer set up for our project, we can use that to install PHPUnit in the vendor directory. Run:

composer require --dev phpunit/phpunit

Composer also automatically downloads and installs all the libraries that PHPUnit depends on, and the dependencies of its dependencies, etc.

We use the --dev option because PHPUnit is a tool for developers, and not a library that our program would rely on in production. If we wanted to prepare a copy of our program to install on a server, we would use composer install --no-dev install any libraries we need and set up the autoloader but leave out PHPUnit.

When we ran the require command composer.json edited our composer.json file to record our updated requirements. composer.json should now look like:

{
    "autoload": {
        "psr-4": {
            "AModeratelyShortPhpTutorial\\": "src/"
        }
    },
    "require-dev": {
        "phpunit/phpunit": "^9.0"
    }
}

The last major release of PHPUnit was 9.0, so composer has assumed we will always want whatever the latest PHPUnit release in the 9 series is. The 10 series is not expected to be compatible with code written for PHPUnit 9, so composer won't install that unless we edit composer json. Composer works best with dependencies that use semantic versioning.

Composer has also created a new file for us, composer.lock. This has metadata about the exact versions of the packages installed. At the time of writing it shows me that PHPUnit is at version 9.0.1, and I can see the details of 29 other packages that have been installed because PHPUnit depends on them directly or indirectly. The composer show command will output the list of installed packages in a much more concise format.

Writing a test

Let's write our first test. Create a test subdirectory next to src, and write the following in test/PlanetTest.php

<?php declare(strict_types=1);

namespace AModeratelyShortPhpTutorial;

use PHPUnit\Framework\TestCase;

final class PlanetTest extends TestCase
{
    private Planet $SUT; // SUT = Subject Under Test

    public function setUp(): void
    {
        $this->SUT = new Planet('planet name', 0);
    }

    public function test_it_can_accept_immigrant(): void
    {
        $this->assertSame(0.0, $this->SUT->getPopulationSize());
        $this->SUT->receiveImmigrant();
        $this->assertSame(1.0, $this->SUT->getPopulationSize());
    }
}

The first new keyword here is use. This is syntactic sugar that saves us having to spell out the name of the class PHPUnit\Framework\TestCase in full when we use below.

We also meet the extends keyword here. This means that our class is an extension, or subclass, of PHPUnit's TestCase class, which is how PHPUnit is designed to be used. If TestCase has been marked final we wouldn't be able to extend it.

If you don't have PHP 7.4, remember to remove the Planet property type of the SUT, and replace it with a docbloc as we did for the properties of Planet itself.

It's prudent to see a test fail at least once before believing what it says when it passes. To make it fail, comment out $this->populationSize++; in src/Planet.php:

// $this->populationSize++;

Now run the PHPUnit command:

vendor/bin/phpunit test

This will search for any filenames ending in Test.php in the test directory. In each test case any public function whose name starts with test is considered a test. For every test PHPUnit creates an instance of the class, calls the setup function, then calls the test function, records the results, and then throws away the object. So if we had two tests it would call setUp twice. Any object referenced only by garbage is garbage, so when the test case object is thrown away the Planet is thrown away too, and any mutations to the planet will not affect the next test.

The output from PHPUnit should look like:

PHPUnit 9.0.1 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 36 ms, Memory: 4.00 MB

There was 1 failure:

1) AModeratelyShortPhpTutorial\PlanetTest::test_it_can_accept_immigrant
Failed asserting that 0.0 is identical to 1.0.

/tmp/composerPlayground/test/PlanetTest.php:20

FAILURES!
Tests: 1, Assertions: 2, Failures: 1.

Fix the Planet class putting the increment back in, and re-run the PHPUnit command. We should now see some happier output:

PHPUnit 9.0.1 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 35 ms, Memory: 4.00 MB

OK (1 test, 2 assertions)

Writing tests is a huge topic, which we can't cover in detail here. PHPUnit has excellent official documentation. You might want to do Test Driven Development (TDD) and/or Behaviour Driven Development (BDD) and write your tests before writing the production code that they cover.

Some other major test frameworks for PHP are PHPSpec and Behat. These are both designed around the BDD approach, which uses the language of executable specifications rather than tests. A major difference between them is that in PHPSpec, as with PHPUnit, you code in PHP. In Behat you code in a separate language called gherkin, designed to look like English and be readable by people who haven't been trained in programming.