Your First Unit Tests With PHPUnit

In the previous lesson, we learned the basic concepts of testing in general, and then we looked at the various tools available to us for testing in php. In this tutorial, we'll learn our first testing tool, PHPUnit. And we'll walk through a simple example to see how PHPUnit is actually being used to unit test our code.

But please note that this tutorial is for beginners, so we won't look at any advanced techniques in testing (those are for other tutorials). This is intended to give you a taste of how unit testing is actually done.

Install PHPUnit

PHPUnit as any other tool should be installed first. There are number of ways to install it, however for me I like to use composer for that. And again as with most tools, you can install it either globally or locally. In my opinion, for any tool or package my project depends on, I prefer to install it locally, so when other person pull it from github, he would just run composer install, and the whole environment will be set up automatically.

So in your project's directory require it with composer using composer require phpunit/phpunit --dev (if you're not familiar with composer, check out this tutorial). Notice I've specified that this package is a development dependency, and that's true since we only test in development.

Now after running that command, you'll find that it was installed (with other dependencies) in the vendor directory. You can find the PHPUnit's executable file in vendor/bin directory. So to run it, use vendor/bin/phpunit from the terminal. However if you feel this is a little too writing, you can either alias it to something shorter, like this: alias p="vendor/bin/phpunit" (so then just run it with p), or include the path vendor/bin into your $PATH environment variable, so you'll only write phpunit.

Know TDD first

You'd normally think that the way we test is by writing the implementation code first, then we write the tests for it. And that's actually true, it's a way of testing. However, this isn't the case with TDD (Test-driven development). In TDD we start by writing the test first, then we watch it fails (of course because we don't have any implementation yet), then we write the necessary code that makes it pass, then we rerun the test again to check if it really passes, if not, we modify the implementation until the test is green (passes). And then after we see it passes, we start refactoring the code if it's necessary (which is by improving the structure of the code without changing its behavior).

So we conclude that the TDD cycle is: RED, GREEN, REFACTOR.

Actually, for me, I've found that TDD helps me write better code, because thinking about what to test first gives me a clear vision of what to do. Not just that, writing tests before implementation ensures almost 100% code coverage. Of course there are many other advantages. Therefore, I've decided to use TDD for the example bellow.

Example: Build a simple calculator

We'll walk through a trivial example of building a simple calculator. Clearly, I've chosen a simple thing to work with in order to simplify the concepts of unit testing and to focus more on testing rather than other things. You can find the source code for this tutorial on github (the button at the end of this tutorial).

Setting up the directory's structure

Before we dive in and start writing code, we need to set up our project's directory structure. So first create a src/ directory to contain our implementation code. And create a tests/ directory to contain our test code. Then don't forget to install PHPUnit via composer.

Your first unit test

Now let's begin with our first test. Obviously, when we talk about calculators, the first thing we think of is addition. So this is what would be our first test about.

As I've mentioned above, we are going to use TDD, which means we'll write the test first. So in your tests/ directory create CalculatorTest.php. And we're following a naming convention here, we have to suffix our test's filename with Test, without that, PHPUnit won't recognize our test files.

In that file write the following code:

<?php

class CalculatorTest extends PHPUnit_Framework_TestCase {


}

I know the class is empty, just ignore this for now. Let's try to run PHPUnit on this file and see what will happen. To run PHPUnit simply execute this command from terminal: vendor/bin/phpunit --colors tests/CalculatorTest. This command will run all test cases within the file CaclulatorTest (it's empty now). When it finishes, you'll see something similar to this:

Running an empty test file

That error was expected, we didn't have any tests to run, so it's telling us that there are no tests in your test file. The output coloring in PHPUnit is turned off by default, so we use the option --color to display the output in colors (don't worry if this command is too long, we'll see how to automate this later). Of course running each test file one by one isn't practical, so you can instead specify the tests directory that contains all the tests you want to run. So we can strip out CalculatorTest from our command, and it becomes vendor/bin/phpunit --colors tests.

As you notice, our test class extends PHPUnit_Framework_TestCase, and that is necessary to be able to use the asserting functions that PHPUnit provides. And if you're not familiar with what asserting functions are, they are the functions you'd use to check if the result of a certain functionality was as expected, for example there is a function to test the equality of two values, or to check if a certain value is a truthy value, or if an array contains a specific value, and all sort of these things. So you need to know some of them (not all) to be able to test things. For example, in this example we'll only use $this->assertEquals(expected, actual), which is used to test the equality of two values, and you would usually write the expected value in the first argument (e.g. 4), and the actual value that you want to test in the second argument (e.g. $result). To see more of these functions check out the documentation.

So let's add the test case of addition:

<?php

class CalculatorTest extends PHPUnit_Framework_TestCase {

    public function testAddition()
    {
        $calculator = new Calculator();

        $result = $calculator->add(2, 2);

        $this->assertEquals(4, $result);
    }
}

Notice how we write test cases in PHPUnit, we have to prefix the method name with the word test, without this, PHPUnit won't recognize it. An alternative way, is to use annotations like this:

<?php

class CalculatorTest extends PHPUnit_Framework_TestCase {

    /**
    * @test
    */
    public function it_adds_numbers()
    {
        $calculator = new Calculator();

        $result = $calculator->add(2, 2);

        $this->assertEquals(4, $result);
    }
}

For me, I prefer the latter one since it allows me to write the method name however I like (more flexible, and good for documentation). So it's up to you and your preference. Choose whatever you prefer and be consistent with it.

Now, if you run the tests, you'll get a fatal error. And that's simply because you don't have a Calculator class yet. So create it in your src/ directory and name it Calculator.php. Then you have to include it in your test file (tests/CalculatorTest.php), so just after <?php add require_once 'src/Calculator.php';.

Define the class Calculator in Calculator.php like this:

<?php

class Calculator {

}

If you run it again, you'll get another fatal error telling you that you don't have add() function. So add it to the class (notice this pattern where our tests tell us what to do, pretty cool!).

<?php

class Calculator {

    public function add($first, $second)
    {

    }
}

Now you won't get any other fatal errors, instead you'll get a failing test. And our failing test is telling us that it was expecting 4 but the result is null. To fix it, simply write the code that would return the summation of both numbers, like this:

<?php

class Calculator {

    public function add($first, $second)
    {
        return $first + $second;
    }
}

Run the test, and congratulations! now you're looking at your first passing test.

Another feature

Our calculator now can add only two numbers. Let's add another feature to it so it'd be able to add any number of numbers.

Again we start with our test. Add this test case to your CalculatorTest.php.

/**
 * @test
 */
public function it_adds_more_than_two_numbers()
{
    $calculator = new Calculator();

    $result = $calculator->add(2, 2, 3, 4);

    $this->assertEquals(11, $result);
}

Now run the tests, you should get one passing test and one failing test. And that's because we haven't given the calculator the ability to add more than two numbers. To do so, change the implementation of the add() method in the Calculator class to this:

<?php

class Calculator {

    public function add()
    {
        $numbers = func_get_args();

        $result = array_reduce($numbers, function($carry, $item) {
            return $carry + $item;
        });

        return $result;
    }
}

What we have done is so simple, just grab all the arguments of the add() function (in form of an array), then use array_reduce to add all numbers in the array, then return the result. Actually the implementation isn't so important, you can solve the problem however you like, and that's ok as long as the tests pass.

Now if you run the test, they should all be green!

Maybe you're asking, why didn't I use the same test case (the first one) to test the other feature? Actually you could, but for me, when I'm testing I like to make each feature explicit, maybe this example is so trivial, but when we're testing a more complicated class, it's better to make each feature as explicit as possible. And really for this example, you can delete the first test case and rely on the other one, but as I said, explicitly is better.

Now you understand how the cycle of unit testing works, so go and add more features to your awesome calculator! For example, add the rest of the operations that any calculator would have (such as: multiplication, subtraction …etc).

Autoload your classes

As you've might noticed, for each test class (although we only have one!) you'd have to include the classes needed by your test (e.g. Calculator.php). And that's not practical when you have dozens of classes in your system. A better way is to autoload all the needed classes via composer (if you don't know how, check out this tutorial). So you might say, "well that's easy just include vendor/autoload.php in each test file you have". Yeah that's right, but that would be also another thing we have to do for each test file we create. So wouldn't it be cool if all of your classes can be loaded automatically without even the need to include vendor/autoload.php in each test file. And actually that's possible, all you have to do is to reference vendor/autoload.php as the value of the bootstrap option that PHPUnit has. I mean all you have to do is something like this, vendor/bin/phpunit --colors --bootstrap="vendor/autoload.php" tests/.

And that's so helpful because now, as we'll see in the next section, we can include all of our options in a configuration file, so we'll only have to run vendor/bin/phpunit and all of our options will be used as specified in the configuration file.

PHPUnit configuration

As I said before, including all the options for PHPUnit when we run our tests isn't practical, because imagine if we have other options we want to use, that would be a lot of writing. And yet, if we share the project, it can be difficult to run the tests without knowing all the needed options and what suites are needed to be included. Fortunately, PHPUnit gives us a way to include all of our options in a configuration file, named phpunit.xml. Now let's create a one and put all of our options and suites (we only have one) into it.

In your project's root directory create a phpunit.xml file and put the following into it.

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

         <testsuites>
            <testsuite name="default">
               <directory>tests</directory>
            </testsuite>
         </testsuites>
</phpunit>

As you can see, all the options we've been providing within the command now are represented as attributes on the tag. So we turned on the colors, and we specified that it should include the composer autoloader before running each test, and then we specified that all errors, warnings and notices should be converted to exceptions.

Now inside the tag we would put all of our test suites. And you can think of a test suite as it is a way to group all related tests together, so we can run an individual suite alone. And of course we need to give a name for each suite, in our case we only have one suite, and I named it "default". Within that suite we can specify the corresponding directory path, in our case it's "tests/" directory. If you want to run a specific test suite, specify it as an option like this: vendor/bin/phpunit --testsuite="default", while if you don't specify any suite, PHPUnit will execute all of them.

With that file, we can now use vendor/bin/phpunit (or just phpunit if vendor/bin is in your $PATH) instead of vendor/bin/phpunit --colors --bootstrap="vendor/autoload.php" tests/. Look how easy it makes our lives!

What should we do next?

If you've added more features to the calculator (such as all the other operations), you'd notice that the class would get larger and messier (not a big deal with this simple example, but imagine that it's a more complicated project). And when you feel that thing, you should go for refactoring. Because remember, the third part of the TDD cycle is Refactoring. Refactoring is a big topic on its own, so I'll dedicate other tutorials on it.

Conclusion

Of course we've barely scratched the surface here. It's just meant to give you the vision of how unit tests in their basic form are used. And as I've said before, there are many more advanced topics regarding unit testing. So sure there will be more coming tutorials on this topic in the future.

Please don't hesitate to leave any questions or feedbacks you have bellow in the comments.