Skip to content
PHP Laravel

PHPUnit: What, Why, How?

16 min read

This is a brief introduction to Unit Testing with PHPUnit and is based on a lightening talk I gave at Sheffield PHP on the 18th January 2018. We'll take a look at what unit testing and PHPUnit are, why we would use them and finally look at how we can start writing tests.

The unit test examples shown in this blog post can be found over on GitHub.

What is Unit Testing and PHPUnit?

PHPUnit is a framework independent library for unit testing PHP. Unit testing is a method by which small units of code are tested against expected results.

Traditional testing tests an app as a whole meaning that individual components rarely get tested alone. This can result in errors going undetected for a long time and it can be difficult to isolate the cause of a specific bug within the code.

With unit testing we test each component of the code individually. All components can be tested at least once. A major advantage of this approach is that it becomes easier to detect bugs early on. The small scope means it is easier to track down the cause of bugs when they occur.

As an example, imagine we have an ecommerce website. We might traditional test this by opening the site in the browser, searching for a product then adding this to the basket and checking the basket total looks as we'd expect. If we were to unit test this process we might write a test to ensure we can search for a specific product, write another to ensure we can add a product to the basket and then a test to ensure that the basket total sums correctly.

I first started unit testing a few years ago with CakePHP. The CakePHP framework has included support for comprehensive testing since its early days and began to come integrated with PHPUnit from version 2. It was Cake's integration of PHPUnit that got me started.

These days most modern PHP frameworks come with PHPUnit integration including Laravel, Symfony and CakePHP. CMSs including Wordpress and Drupal also use it for testing, as well as the ecommerce platform Magento.

Unit Testing is like exercise

Gym

Writing tests can be hard, and writing good tests can be even harder. I think a good analogy of unit testing code is physical exercise. At first it seems like a good idea, but you need to motivate yourself to get started; in the short term it can seem pointless as you fail to see any major improvements; but then in the long term you start to see the obvious benefits. Suddenly the effort put in at the start seems to be paying off!

Benefits of Unit Testing

So what are the benefits of unit testing?

To start with tests make it easier for us to refactor code. Over time projects change. Unit tests give us some confidence that updates to code won't break any existing functionality that has previously been thoroughly tested. We can refactor code and run our tests to ensure that it still works as originally intended.

Unit testing can also lead to faster debugging as it is easier to isolate the cause of a bug by identifying where a test fails. When bugs do show up in our code we can also write tests to ensure that these known issues do not reoccur further down the line. I think this last point is particularly important; I have seen many times where multiple developers work on a piece of code and bugs keep creeping back in due to a lack of understanding of all use cases of a method. This brings me to another benefit…

The tests can provide documentation to an app as they document expected uses of code. If you are new to part of a project's code you should be able to look at the relevant tests to see the expected outcomes of it's functionality.

One thing that unit testing won't do is eradicate all bugs from an app. However, it should reduce them and prevent the recurrence of known bugs.

TDD

A further benefit of unit testing can be found by coding with the Test Driven Development approach, or TDD. This is where we would write our tests before even starting to develop the code it will be testing.

Taking the TDD approach can help encourage us to write better code by making us think about what each component of the code is doing. As a result we can develop well-defined code with clear responsibilities.

There have been numerous studies into the benefits of Test Driven Development. It has been found that it can reduce the density of bugs in production code by 40-80%! Obviously writing the tests in the first place will lead to additional work; but if you think how long you spend trying to resolve a single bug and then take into account the number of bugs that might be discovered this reduction is clearly beneficial.

TDD reduces production bug density by 40–80%

Installing PHPUnit

There are a few ways of installing PHPUnit, but one of the simplest is to use Composer:-

composer require --dev phpunit/phpunit

We’re using the --dev flag as we only need this installed for our dev environment, tests shouldn’t run in production. If you’re using a PHP framework this is possibly already set as a requirement.

If you’re not using Composer you can also download the phar file. You’ll find full installation details on the PHPUnit website.

The unit test examples that will follow will assume we’re using Composer to install PHPUnit.

Writing tests

We want to aim to test as much of our code as possible. As a result we should be attempting to test each possible path through our code. That means we want to test for both the successes and failures. As a rule of thumb we should have a test method for each expected outcome of a method.

Our test methods need descriptive names. So something like testProductCanBeAddedToBasket. Our test classes want to be named after the class being tested.

The test class name should end with the word Test and extend \PHPUnit\Framework\TestCase. If you’re using a framework it might have a wrapper for PHPUnit’s TestCase, in which case use that instead. These often add additional methods to aid testing so are worth using. For example, Laravel tests should extend \Tests\TestCase.

Test methods want to be prefixed with test (unless you are using the @test annotation). These methods want to be public and should contain at least one assertion. Test methods should never be dependent on one another; it shouldn’t matter if testA runs before testB, you should be able to run each in isolation.

<?php
use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
	public function setUp()
	{
		// Set up the test class here
	}

	public function testSomething()
	{
		// Write test assertions here
	}

	public function tearDown()
	{
		// Clean up
	}
}

The setUp and tearDown methods are run before and after each test method respectively. We use these to set up and clean up the test methods. We can use these methods to prepare fixed data for our tests, this is known as writing a Fixture.

When you find yourself needing to test data from a database these are a good place to populate and remove test data. For example, if you are using Laravel you can use Model Factories to seed a test database in the setUp and reset the database in the tearDown method. Remember you want to start each test with a clean slate.

There are also setUpBeforeClass and tearDownAfterClass methods that run before and after a test class is run, but use these with caution.

Assertions

The way we actually test our code is by using PHPUnit’s assertion methods. These are used to test for expected behaviour of units. PHPUnit provides many of these for evaluating tests. For example:-

  • assertEmpty($result)
  • assertEquals($expected, $result)
  • assertTrue($result)
  • assertFalse($result)
  • assertInstanceOf($expected, $result)

The method names should be fairly self explanatory, but you’ll find full documentation of these on the PHPUnit website along with a complete list of the assertion methods available.

If you’re using a PHP framework you may find that it provides additional methods for testing on top of those provided by PHPUnit which are well worth checking out.

Remember that we want to test for failures as well as successes. We can test for exceptions using $this->expectException($exception). For example, if we have a method for retrieving a product we might want to test that a product is returned and also that a NotFoundException is thrown when one is not found.

An example test

Let’s take a look at a really simple test using one of the assertions we’ve just looked at. Consider a class named Average that provides methods for determining different types of averages. One of the most common types of averages is the mean average which we can write a test for like this:-

<?php

use drmonkeyninja\Average;
use PHPUnit\Framework\TestCase;

class AverageTest extends TestCase
{
	public function testCalculationOfMean()
	{
		$numbers = [3, 7, 6, 1, 5];
		$Average = new \Average();
		$this->assertEquals(4.4, $Average->mean($numbers));
	}
}

By using assertEquals we check that the value returned by the mean method is the value we would expect for the provided numbers.

A further test

Let’s consider another test as part of our AverageTest class. This time we want to test for a median average:-

public function testCalculationOfMedian()
{
    $numbers = [3, 7, 6, 1, 5];
	$Average = new \Average();
    $this->assertEquals(5, $Average->median($numbers));
}

You may have noticed that we’re initiating the Average class each time we test. We can simplify things a little here by moving this to the test class’ setUp method that comes as part of PHPUnit. So our test class would look like:-

<?php

use drmonkeyninja\Average;
use PHPUnit\Framework\TestCase;

class AverageTest extends TestCase
{
    protected $Average;

    public function setUp()
    {
        $this->Average = new Average();
    }

    public function testCalculationOfMean()
    {
        $numbers = [3, 7, 6, 1, 5];
        $this->assertEquals(4.4, $this->Average->mean($numbers));
    }

    public function testCalculationOfMedian()
    {
        $numbers = [3, 7, 6, 1, 5];
        $this->assertEquals(5, $this->Average->median($numbers));
    }
}

That’s a lot better. We could of course have also moved our $numbers array to the setUp method too, but it’s important to remember that when writing tests you want to keep things simple. Your tests should read like a form of documentation outlining the expected usage of your code. We should also really be including a second test for the median method as its outcome depends on whether we pass an even or odd number of values to it. So for the second test, say testCalculationOfMedianForEvenNumberOfValues we would want to pass a different array of numbers.

If the tests begin to get overly complicated not only does it make it harder to gain this understanding of the code, but also risks introducing bugs into the tests themselves.

Give it a try

I’ve added the above tests to a repository on GitHub that you are free to download and try yourselves. If you are new to unit testing try and extend the existing tests to include a test for a median average with an even set of numbers as described above. If you succeed at that, then try adding a new method to the Average class for determining the modal average (where you find most common number in a set) and an appropriate test; try taking a test driven development approach and write the test before the method in Average.

Testing with PHP frameworks

Frameworks like Laravel and CakePHP come with PHPUnit integration built in so make it easy to get started with testing. You can quickly create a test class using Laravel’s artisan and CakePHP’s bake from the command-line:-

php artisan make:test <name>
bin/cake bake test <type> <name>

As previously mentioned you may find that the framework provides additional methods for writing your tests. For example, Laravel provides a number of methods that wrap up some of the PHPUnit concepts so that you can reduce the amount of code that you need to write.

Mocking

Mocking is an important part of unit testing. It’s beyond the scope of this blog post, but I couldn’t give an introduction to PHPUnit without mentioning it.

When we write unit tests we only want to test our own code and not dependencies (which should have their own tests). For this we can create mock objects that will be used to override objects so that we can focus on testing our own code. Mock objects allow us to replace a dependency in our code with an object of predefined behaviour. For example, if our code interacts with an API we can mock the API object to remove any unstability from our tests.

So say we have two classes, class A and class B; and let’s say class A depends on class B. Both classes would want unit tests, but where class A depends on the result from a method in class B we want to isolate this by creating a mock object so that the tests don’t become reliant on what another bit of code is doing. For example, if we were building an app that sets up tweets to be posted on Twitter we might have a class that retrieves the data that depends on another class that interacts with Twitter’s API to send out the tweets. We would want to test our class for retrieving the data to send to Twitter in isolation of actually sending them. So we could create a mock object for the class that will interact with the API so that we can just test our code for preparing the tweets. When testing we do not want to actually be sending out messages to Twitter each time!

PHPUnit supports Prophecy out-of-the-box to create mock objects. If you seriously want to get into unit testing this is well worth investigating when you’re ready to learn more.

Running tests

Tests are usually run from the command-line with the phpunit command. Assuming we’ve installed PHPUnit via Composer as described above we can run it like this:-

./vendor/bin/phpunit

Unless you have a PHPUnit configuration file – I’ll briefly touch on this in a moment – you will want to pass a file or folder name for the test or tests you want to run:-

./vendor/bin/phpunit tests/AverageTest.php

We also have an option of just running a single unit test using --filter:-

./vendor/bin/phpunit tests/ --filter testCalculationOfMean

When you run your tests it will output how many assertions have been tested and succeeded. For a successful pass you should see something like this:-

PHPUnit response showing 2 tests and 2 assertions have succeeded
PHPUnit success response

If something goes wrong it will show which assertion failed and provide some additional details:-

PHPUnit response showing that 1 test has failed
PHPUnit failed test response

As well as running tests from the command-line many IDEs and editors provide support for PHPUnit. PHPStorm comes with support for running your unit tests; and editors like Sublime Text and Visual Studio Code have plugins available for doing this. Once set up you’ll be able to automatically run tests on save.

It is also worth investigating automation of your tests. For example, using continuous integration for running your tests when committing to a repository using a tool like TravisCI. This will mean that whenever someone pushes a change to the code’s repository the tests will run to ensure the change doesn’t break anything. When working as part of a team this is particular useful. You will often see open-source projects use this setup to ensure merge requests are good.

Configuring tests

I’m not going to go into detail on this, but we can also add a phpunit.xml file to the root of our project to configure how we want PHPUnit to run. While not necessary for running your tests it can make life simpler. Using a config file allows us to define where the tests are so that we don’t need to pass them everytime we run phpunit; we can also define things like env variables to configure our app specifically for testing.

As an example, check out the phpunit.xml file on my unit tests example over on GitHub.

Again, if you are using a PHP framework it is likely to come packaged with a PHPUnit config file already set up for you. You should be able to run your tests straightaway from this, but check out the file and make tweaks where you need to.

Test as much as possible

So to wrap things up, remember that with your unit tests you want to be testing as much of the code as possible. That includes both trivial and non-trivial functionality; and both the successes and failures. It is unlikely that you will be able to test every bit of your code, but it is a good idea to aim for over 90% coverage.

There are tools out there that work with PHPUnit to help you determine what percentage of your code is being covered by the tests you write that are worth looking into once you start to get serious about writing your tests.

Finally, don't forget Integration Testing

Unit testing is great and will help you reduce the number of bugs in your code, but it has its limitations. Remember that with unit testing we are testing small isolated bits of code. It’s important that we still test how these units work as a whole. Integration testing is where we ensure that all the components of the code work together.

At the start of this week I spotted a fun analogy to the importance of performing both unit and integration tests on Twitter. The analogy shows a sensor triggered soap dispenser and a plug socket; both will have been tested to ensure they work alone, but the configuration has the soap dispenser above the sockets. As a result, attempting to use the socket causes soap to be dispensed onto the plug. Two unit tests, zero integration tests.

If you are not already unit testing your code I hope this has inspired you to start. Remember, like my earlier exercise analogy it can be difficult to get started but in the long term you will really benefit from writing tests.

© 2024 Andy Carter