[Developer says] Unit Testing with PHPUnit

Introduction

"Hello world!" This phrase should be common to all developers since it's the first thing you get to do when learning a programming language. However, no actual program -especially modern web apps- is as simple as your first "hello world" program. Code complexity can escalate very quickly and debugging can become really challenging. That's exactly where testing comes into play. Tested code is the only way to make sure your application will work as expected under any circumstances. Unit testing is a software testing method by which individual units of source code (methods) are tested in order to make sure they fulfill our expectations. codebender is written using the Symfony2 framework and PHPUnit is one of the main tools we use to unit test our code. In this tutorial, we'll go through some examples of how to write basic tests, and explain the main aspects of unit testing.

A simple test

Suppose we have a class like the one below:

<?php
class Adder
{
    /**
     *  Returns the sum of x and y
     **/
    public function add($x, $y)
    {
        return $x + $y;
    }
}

The testing class for our Adder::add class method would be:

<?php
class AdderTest extends PHPUnit_Framework_TestCase
{
    public function testAdd()
    {
        $adder = new Adder();
        // normal input
        $this->assertEquals(5, $adder->add(3, 2));
        // null input is handled as 0 by the `+` operator
        $this->assertEquals(3, $adder->add(3, null));
        // string input is handled as 0 too
        $this->assertEquals(1, $adder->add(1, 'hello'));
    }
}

Adding both classes in a PHP file (say test.php), you can run your test like phpunit test.php and the output should look like the one below:

PHPUnit 4.8.6 by Sebastian Bergmann and contributors.

.

Time: 162 ms, Memory: 4.50Mb

OK (1 test, 3 assertions)

where the dot indicates a successful test (hooray!!).

assertEquals is a built-in PHPUnit method that can verify that the second argument (the actual) is the same as the first argument (the expected).

Changing the last assertion of our test to:

<?php
        $this->assertEquals(5, $adder->add(1, 'hello'));

will end up in this:

PHPUnit 4.8.6 by Sebastian Bergmann and contributors.

F

Time: 161 ms, Memory: 4.75Mb

There was 1 failure:

1) AdderTest::testAdd
Failed asserting that 1 matches expected 5.

test.php:20 (that's the line of the faulty assertion)

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

where the F indicates an assertion in our test failed to be verified.

PHPUnit includes several built-in methods like assertArrayHasKey, assertTrue, assertFalse,assertArraySubset, assertContains, etc which will further help you verify the output of your methods. This basic example should not differ much from a test of a more complex function. Remember that unit tests are meant to be small and effective. The purpose of a unit test is to handle the method-under-test as a black box, ignoring the context where this method should be placed.

However, your code (like most modern OOP PHP programs) may use external resources. In that case, you can't use the real resources otherwise your test is no longer a Unit, but an Integration test.

Mock objects and Stubs

In a typical web application like codebender, your code will most likely depend on other components (services, etc) that cannot be used during a test. For example, your application might use a third-party service for logging users in. As a result, you cannot use the actual service and mix test with production data.

Writing the code

An authentication service provided this code, which you must integrate to your application.

<?php
/**
 *  A class created by your authentication provider.
 *  You can use it's code to log users in your app.
 **/
class Authenticator
{
    protected function validatePassword($password)
    {
        if (/* password matches the one stored in your auth provider's database */) {
            return true;
        }
        return false;
    }

    public function logIn($password)
    {
        if ($this->validatePassword($password)) {
            return 'OK'; // User's password was ok
        }
        return 'ERROR'; // Invalid password provided
    }
}

Using the provided Authenticator class, our code should look like this:

<?php
class myAuthenticator
{
    protected $authenticator;

    public function __construct(Authenticator $authenticator)
    {
        $this->authenticator = $authenticator;
    }

    public function getAuthenticationForPassword($password)
    {
        return $this->authenticator->logIn($password);
    }
}
Setting up the test
<?php
class myAuthenticatorTest extends PHPUnit_Framework_TestCase
{
    // Tests the case that everything is ok
    public function testGetAuthenticationForPasswordOk()
    {
        // validatePassword method is stubbed
        $externalAuthenticator = $this->getMockBuilder('Authenticator')
                                        ->setMethods(['validatePassword'])
                                        ->getMock();

        // we force the stub to return true (not actually executed)
        $externalAuthenticator->expects($this->once())->method('validatePassword')
            ->willReturn(true);

        $myAuthenticator = new myAuthenticator($externalAuthenticator);
        $this->assertEquals('OK', $myAuthenticator->getAuthenticationForPassword(123456));
    }

    // Tests the case of a wrong password
    public function testGetAuthenticationForPasswordError()
    {
        // validatePassword method is stubbed
        $externalAuthenticator = $this->getMockBuilder('Authenticator')
                                        ->setMethods(['validatePassword'])
                                        ->getMock();

        // now we force the stub to return false (not actually executed)
        $externalAuthenticator->expects($this->once())->method('validatePassword')
            ->willReturn(false);

        $myAuthenticator = new myAuthenticator($externalAuthenticator);
        $this->assertEquals('ERROR', $myAuthenticator->getAuthenticationForPassword(123456));
    }
}

The getMockBuilder method is where everything begins when creating a test double. It returns an object that extends the provided class and can be used in every context where the Authenticatorclass can be used. Whether or not the class members of the test double are actually executed will depend on how you use setMethods function. The possible cases are:

  • Not using setMethods at all
<?php
    $mockObject = $this->getMockBuilder('ClassToBeMocked')
                        ->getMock();

In this case, all class methods of the ClassToBeMocked class are replaced by configurable test doubles (dummies). Their behavior is changed. That is, during the test execution they will return whatever you declare they must return using the ->willReturn(return_value) statement. An object containing only dummy methods is called a stub.

  • Using setMethods(null)
<?php
    $mockObject = $this->getMockBuilder('ClassToBeMocked')
                       ->setMethods(null)
                       ->getMock();

None of the class methods are replaced with test doubles. They all maintain their original behavior. An object containing one or more non-dummy methods is called a mock.

  • Using setMethods(['methodName_1', 'methodName_2'])
<?php
    $mockObject = $this->getMockBuilder('ClassToBeMocked')
                       ->setMethods(['method_1', 'method_2'])
                       ->getMock();

In the example above, methods method_1 & method_2 will be replaced with test doubles, whereas all the rest methods of ClassToBeMocked will maintain their original behavior.

Now it should be pretty clear that validatePassword method is replaced by a dummy and logInmethod will be executed normally. We forced the dummy to return true or false, depending on the purpose of our test.

Note: final, private and static methods cannot be stubbed or mocked. In a well-designed class, you should be able to test your private/final/static methods through the execution of public and protected ones.

Cool trick

If the class you're trying to mock with getMockBuilder has external dependencies, you can use ->disableOriginalConstructor() method in order to avoid having to set those dependencies up.

<?php
    $mockObject = $this->getMockBuilder('ClassToBeMocked')
                       ->disableOriginalConstructor()
                       ->getMock();

Conclusion

The techniques described above should be enough to help you in getting started with Unit Testing. However, you will need to spend a lot of time writing tests of your own in order to have your code tested in a satisfactory level.

Moreover, you shouldn't forget that Unit tests only test the logic of your code. Besides logic bugs, a PHP program could have defects like high complexity, overcomplicated expressions, copy pasted code, etc. There are tools that detect such defects and help you keep your code simple and maintainable.

I'd suggest getting to know Unit testing through PHPUnit's official page and Juan Treminio's 5-part Unit testing tutorial which has been extremely useful to me.