Introduction to PHPUnit

As developers, we spend a lot of time testing code that we’ve written. Generally, this is a manual process where we write a little code and then type in some inputs to verify we get what we expect. The downside to this process is that it’s very time-consuming to manually enter a bunch of input to see if one of them now fails after we’ve made a change.

What if we could use an automated testing tool to do the hard work for us? That’s where PHPUnit comes into the picture.

What is PHPUnit?

PHPUnit is a unit testing framework written in PHP that uses PHP code to test PHP code.

The concept of a unit testing framework isn’t something we’ve discussed before so let’s review that.

[U]nit testing is a software testing method by which individual units of source code … are tested to determine whether they are fit for use.

  • https://en.wikipedia.org/wiki/Unit_testing

Ideally, a unit test is small and can be run quickly. There are exceptions to this rule but for the most part, the focus is on speed and how just a single class acts.

We can also use PHPUnit to write integration tests. Integration tests are used to test how multiple classes interact with each other. These tend to be slower to set up and slow to run.

Ideally, our tests won’t interact with anything external to our application such as external APIs, services, and databases. Sometimes we can’t get around this limitation. For example, we might have a reporting module where its primary function is to get data from a database and we need to verify that we can do this.

There are many other testing frameworks for testing in PHP but PHPUnit is the de-facto testing framework for PHP. Some other testing frameworks to look into include Codeception, Behat, and phpspec.

PHPUnit is was created and is now maintained by Sebastian Bergmann. Sebastian Bergmann is one of the key members of the PHP community so his name may come up for other topics.

Installing PHPUnit

There are several ways to install PHPUnit.

image of other ways

Several of them appear to be around from the days before Composer existed but unless there’s a very specific reason you need the PHAR version we recommend PHPUnit be installed using Composer.

composer require --dev phpunit/phpunit ^9.5

There’s a “great internet debate” over if PHPUnit should be installed globally or in every project. The argument to installing it globally is that instead of having to specify the full path to PHPUnit in our project (./vendor/bin/phpunit) we can save ourselves a little time by having it in our path and just typing the name (phpunit). This works great if we only have one project on our system or all of our projects are using the same major version of PHPUnit.

People who are working with multiple projects at different versions of PHPUnit are going to want to install it per project.

Running PHPUnit

PHPUnit is a command-line tool and as a command-line tool, there are a lot of parameters that make it super powerful. This power allows us to run just some of our tests depending on how we call PHPUnit.

Here are some of the ones we use most often.

By default, we can run PHPUnit without any parameters and it will run all of our tests. This is how we’re run it before we deploy our changes to production.

phpunit

We can pass the path of a specific test file we want to run and it will run just the tests inside that file. This is helpful if we want to make sure our changes to a file didn’t affect any other functionality in that class.

phpunit tests/Unit/SuperStringTest.php

We can use the --filter parameter to run tests that contain specific words. This is helpful if we want to isolate our run of PHPUnit to just tests that affect a specific type of test. For example, we might have changed our User class and we want to run any tests that have “User” in the name of the test class or test method.

phpunit —filter User

Finally, we can filter it so we run a specific test in a specific file. This is a good way to run just the test we’re working on to speed us up and not get distracted by other tests failing.

phpunit —filter testAStringCausesIsEmptyToReturnFalse tests/Unit/SuperStringTest.php

When we’re developing a new test we start by running just the new test using the filter/file option above. Once that’s working the way we want it to we’ll run the whole file to make sure we didn’t break a related test. Finally, we’ll run all of the tests to make sure we didn’t affect another class that has a dependency on the class we changed.

While PHPUnit is a command-line tool by default there are some huge efficiency gains from looking into how our IDE can call PHPUnit directly. When we’re manually running our tests we have to make some change in our code, tab over to our terminal (command + tab or ctrl + tab), type in the command we need (which is based on our phpunit —filter testAStringCausesIsEmptyToReturnFalse tests/Unit/SuperStringTest.php could be more than 80 characters), and then we can see the results of our change. With IDE integration there is generally a button we can use to run a single test or a whole file or better yet there should be a keyboard shortcut.

Organization

We love how clean the organization of PHPUnit tests is! All test files are inside a tests folder separate from our application logic. Some testing frameworks have the test file in the same directory as the code under test which is great for finding the test but the separation makes it easier to search if we have to perform a global search and replace. The downside to this is that it’s not always clear how to structure our tests for maintainability.

PHPUnit uses PHP classes to group similar tests together. We almost always want a test class for each application class with logic in our application. We generally don’t write tests for models that have no logic and are only there so we can use our framework’s ORM or that are being used to house constants.

We also recommend a test class for common initial conditions. For example, we have a series of tests that perform different tests for each role a User of our application may have. By grouping them into a class each we can quickly know where to go if we need to make a change to how one of the roles acts. These test classes might also benefit from a shared superclass that has common tests which can be a huge help when another User role is added.

phpunit.xml

The only other thing we’re going to need to get started using PHPUnit is a phpunit.xml file in the root of our project. This file will be loaded automatically by PHPUnit when it loads if it’s in the directory we’re calling the PHPUnit executable from.

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
        </whitelist>
    </filter>
</phpunit>

Writing Our First Test

PHPUnit organizes tests inside PHP classes. We can name our test classes any way we want as long as it’s a valid PHP class name. Each class also needs to ends with “Test” or PHPUnit won’t autoload that file by default. This can be changed by altering the “directory” node in our phpunit.xml file. It also needs to extend \PHPUnit\Framework\TestCase.

<?php
namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

class SuperStringTest extends TestCase
{
    
}

Now that we’ve created our test class we can start adding some tests. In PHPUnit individual tests are just class methods.

There are two ways to do this. The first is to name our test starting with the word “test”.

<?php
namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

class SuperStringTest extends TestCase
{
    public function testAStringCausesIsEmptyToReturnFalse(): void
    {
        $item = new SuperString('Test Data');
        $this->assertFalse($item->isEmpty());
    }
}

The second is to use the @test annotation in the DocBlock for the method.

<?php
namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

class SuperStringTest extends TestCase
{
    /**
    * @test
    */
    public function aStringCausesIsEmptyToReturnFalse(): void
    {
        $item = new SuperString('Test Data');
        $this->assertFalse($item->isEmpty());
    }
}

Which one you choose is a personal or team preference. We learned PHPUnit before the annotation option so we default to prefixing tests with test. It also uses less vertical space which is helpful for presentations/articles.

Each test must call at least one assertion method. Assertion methods are used to assert that a value from our code matches an expected value.

PHPUnit ships with a LOT of assertions and it seems like every release contains more. Assertion methods start with “assert” and then detail what it’s asserting.

$this->assertEquals($expectedValue, $value);
$this->assertFalse($value);
$this->assertGreaterThan($number, $value);

If the value we’re sending the assertion doesn’t match the expected value the test will be marked as “failed” when we run PHPUnit and we’ll get a nice listing of all the failed tests in the output.

./vendor/bin/phpunit tests/Unit/SuperStringTest.php 
PHPUnit 9.5.4 by Sebastian Bergmann and contributors.

Warning:       Your XML configuration validates against a deprecated schema.
Suggestion:    Migrate your XML configuration using "--migrate-configuration"!

F                                                                   1 / 1 (100%)

Time: 00:00.117, Memory: 6.00 MB

There was 1 failure:

1) Tests\Unit\SuperStringTest::aStringCausesIsEmptyToReturnFalse
Failed asserting that true is false.

/var/www/tests/Unit/SuperStringTest.php:13

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

Creating Your Own TestCase

One of the best pieces of advice we can give about how to best structure test code is to create a TestCase class for each application. This will allow us to create helper functions that perform common setup code and create our assertion methods.

<?php

namespace Tests;

use PHPUnit\Framework\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    public function createTestUser(): User
    {
        $user = new User();
        // bunch of initialization
        return $user;
    }

    public function assertUserHasAccess(User $user, Event $event): void
    {
        $this->assertTrue($user->hasAccess($event));
    }
}

What You Need To Know to Nail a Job Interview

  • PHPUnit is a unit testing framework for PHP
  • Tests are put into the tests directory of your project
  • Tests are organized by having one test file per model
  • Don’t be afraid to create test files with common initial conditions
  • Each test must contain at least one assertion method call

Our next article will walk through developing a new feature using PHPUnit so make sure you subscribe to see it.

Are you using PHPUnit? How is it going? Let us know in the comments if it’s going great or if you’re tearing your hair out trying to get it to work.