The Five Types of Test Doubles & How to Create Them in PHPUnit

Did you know that a Mock is only one type of a test double? Most of us use the word “mock” to mean any kind of test double, but there’s actually five different types. It really can help you understand what you’re trying to accomplish with your test if you know a little bit more what you’re doing with your test doubles, so this article will explain the kinds of test doubles, when you use them, how you use them and why.

Test Doubles

A Test Double is any object that stands in for a real dependency in automated testing. Usually in PHPUnit, we make test doubles for other classes, but you can also double built-in PHP functions or closures.

The five types of Test Doubles are:

  • Dummy – Used only as a placeholder when an argument needs to be filled in.
  • Stub – Provides fake data to the System Under Test
  • Spy – Records information about how it is used, and can provide that information back to the test.
  • Mock – Defines an expectation on how it will be used, and with what parameters. Will cause a test to fail automatically if the expectation isn’t met.
  • Fake – An actual implementation of the contract, but is unsuitable for production.

System Under Test

When testing, we refer to the class we are testing as the System Under Test. In the vast majority of cases, you won’t create a test double of this class, instead you’ll use the actual class, and create test doubles for each of it’s dependencies.

Test Doubles in PHPUnit

PHPUnit offers methods for creating test doubles very easily within your unit tests. There are other mocking libraries as well, such as Mockery. You don’t even need to use a library at all though – in order to demonstrate the difference between these types, I’m going to show you how to create them from plain PHP!

Why does this matter?

Knowing the difference between these various types of test doubles, whether they need to return data, or throw an exception from an expectation, and knowing how much code you need to write for a given double, can help you keep your tests small and resilient. If you often need to change multiple tests when you change one part of a dependency, you may be relying too heavily on the larger, more expensive doubles (Mocks, Spies and Fakes) when you could be using a lighter double (Dummy or Stub).

It’s also important to know what kind of test you’re writing, and this influences the type of double. For example, if you’re writing a contract or correctness unit test, you’re going to use Dummies and Stubs – these are often used in the tests which do not concern themselves with communication between the dependencies. Spies, Mocks and Fakes are used in tests which do concern these communications, and these are often called collaboration tests. These Test Doubles often need to be concerned with not only if a method on the collaborator is called, or how often, but also with what arguments.

Dummy

A Dummy is the simplest type of test double, because it just has to fulfill the System Under Test’s type hints, to fill in a blank spot.

Let’s look at a class which calculates if Live Chat should be available on our website. We only want to offer chat between certain business hours, and we’ll also use a Presence service to ensure that during those hours, we only want to offer Live Chat if at least one agent is present.

class ChatAvailabilityCalculator
{
    private $openTime;
    private $closeTime;
    private $currentTime;
    private $presenceService;

    public function __construct(DateTime $openTime,
                                DateTime $closeTime,
                                DateTime $currentTime,
                                PresenceService $presenceService)
    {
        $this->openTime        = $openTime;
        $this->closeTime       = $closeTime;
        $this->currentTime     = $currentTime;
        $this->presenceService = $presenceService;
    }

    public function isChatAvailable(): bool
    {
        $afterOpen       = ($this->currentTime >= $this->openTime);
        $beforeClose     = ($this->currentTime closeTime);
        $inBusinessHours = ($afterOpen && $beforeClose);
        if ($inBusinessHours === false) {
            return false;
        }
        return ($this->presenceService->agentsPresent() === true);
    }
}

We’ll write a couple of tests for the isChatAvailable() method, such as testing that the function returns false when the business isn’t currently open, returns false when there’s no agents present, and that it returns true during business hours when agents are present.

The first test will need a Dummy, because we don’t need to do anything with the Presence Service, but we need to fill in that spot in the constructor.

The Presence Service is a simple interface:

interface PresenceService
{
    public function agentsPresent(): bool;
}

Creating a Dummy from scratch is super easy in PHP 7 thanks to Anonymous classes. In this example, I’ve had to implement the agentsPresent() method otherwise PHP will complain. However, this method isn’t used, so the value you return does not matter. If you have strict_types enabled in your test files, you will need to return something, otherwise the test will fail, but the value isn’t ever used.

/** @test */
public function isChatAvailableFalseWhenOutsideBusinessHours()
{
    $openTime    = new DateTime('8:00 am');
    $closeTime   = new DateTime('5:00 pm');
    $currentTime = new DateTime('6:00 pm');

    $presenceService = new class implements PresenceService
    {
        public function agentsPresent(): bool
        {
            return false;
        }
    };

    $calculator = new ChatAvailabilityCalculator($openTime,
                                                 $closeTime,
                                                 $currentTime,
                                                 $presenceService);
    $this->assertFalse($calculator->isChatAvailable());
}

Stub

The next type of Test Double is a Stub, which is where the double needs to return a specific value for the test. We can continue with our example from the Dummy section, and write the test for the case where the isChatAvailable() method returns false when no agents are present.

For this test, it does matter what I return in the agentsPresent() method body – this is the stubbing part, where I’m specifying if I want true or false returned in order to impact the result of the test.

/** @test */
public function isChatAvailableFalseWhenNoAgentsPresent()
{
    $openTime    = new DateTime('8:00 am');
    $closeTime   = new DateTime('5:00 pm');
    $currentTime = new DateTime('4:00 pm');

    $presenceService = new class implements PresenceService
    {
        public function agentsPresent(): bool
        {
            return false;
        }
    };

    $calculator = new ChatAvailabilityCalculator($openTime,
                                                 $closeTime,
                                                 $currentTime,
                                                 $presenceService);
    $this->assertFalse($calculator->isChatAvailable());
}

For the final test, we would also use a Stub, but this time it returns true – as does our System Under Test:

/** @test */
public function isChatAvailableTrueWhenAgentsPresent()
{
    $openTime    = new DateTime('8:00 am');
    $closeTime   = new DateTime('5:00 pm');
    $currentTime = new DateTime('4:00 pm');

    $presenceService = new class implements PresenceService
    {
        public function agentsPresent(): bool
        {
            return true;
        }
    };

    $calculator = new ChatAvailabilityCalculator($openTime,
                                                 $closeTime,
                                                 $currentTime,
                                                 $presenceService);
    $this->assertTrue($calculator->isChatAvailable());
}

Spy

Spy is a test double which records the way in which it’s used – or “spies” on it – and later allows you to access that recorded information to make an assertion in your test.

Most people use Spies in tests which are technically integrated, and they want to use most of the methods of a collaborating class, but need to stub or mock some parts of it, to assert a method was called, or to check the values it was sent. Sometimes it can be easier to write a test using a Spy instead of a Mock, when you’re working with legacy code. They are really useful if you need to rely on private methods in the dependency, and don’t want to replace it with a partial Mock. Spies can also be used in isolated tests.

Let’s look at an example of a Spy, then we’ll compare it with a Mock.

For these examples, we’ll consider a User Registration controller/service. In this example, our User model has multiple string parameters – we might want to test that the correct values were set for each argument, and the User is correctly passed to the Persistence Layer.

class User
{
    public $username;
    public $emailAddress;
    public $favoriteColor;

    public function __construct(string $username,
                                string $emailAddress,
                                string $favoriteColor)
    {
        $this->username      = $username;
        $this->emailAddress  = $emailAddress;
        $this->favoriteColor = $favoriteColor;
    }
}

interface UserPersistenceDriver
{
    public function save(User $user): bool;
}

class UserRegistration
{
    private $persistenceDriver;

    public function __construct(UserPersistenceDriver $driver)
    {
        $this->persistenceDriver = $driver;
    }

    public function register(string $username,
                             string $favoriteColor,
                             string $emailAddress): Response
    {
        $user = new User($username, $emailAddress, $favoriteColor);
        $this->persistenceDriver->save($user);
        return new Response(201);
    }
}

So in order to test this with a Spy, we’ll create an implementation of the UserPersistenceDriver which records the value sent to it, so we can look at it later.

class UserRegistrationTest extends \PHPUnit\Framework\TestCase
{
    /** @test */
    public function registerSendsUserToPersistence()
    {
        $username      = "j.doe";
        $favoriteColor = 'blue';
        $emailAddress  = 'jdoe@example.com';

        $driver = new class implements UserPersistenceDriver
        {
            public $user;
            public $called = false;

            public function save(User $user): bool
            {
                $this->called = true;
                $this->user   = $user;
                return true;
            }
        };

        $action = new UserRegistration($driver);
        $action->register($username, $favoriteColor, $emailAddress);

        $user = $driver->user;
        $this->assertTrue($driver->called);
        $this->assertEquals($user->username, $username);
        $this->assertEquals($user->favoriteColor, $favoriteColor);
        $this->assertEquals($user->emailAddress, $emailAddress);
    }
}

In this test, we use the Driver Spy to record the User object sent to it’s save() method, and then we can access it later to perform assertions.

Spies are also super useful when you want to make sure a method in the dependency does not get called. Let’s add some validation to the register() method, to ensure the favorite color is one in a predefined list.

Here’s our updated method, with a small list of allowed colors:

public function register(string $username,
                         string $favoriteColor,
                         string $emailAddress): Response
{
    $colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'pink'];
    if (in_array($favoriteColor, $colors, true) === false) {
        return new Response(422);
    }
    $user = new User($username, $emailAddress, $favoriteColor);
    $this->persistenceDriver->save($user);
    return new Response(201);
}

We’ll write a new Spy that only tracks if the save() method was called, and then assert that it was not called at the end of the test.

/** @test */
public function registerDoesNotPersistUserWhenInvalidColor()
{
    $username      = "j.doe";
    $favoriteColor = 'rainbow';
    $emailAddress  = 'jdoe@example.com';

    $driver = new class implements UserPersistenceDriver
    {
        public $called = false;

        public function save(User $user): bool
        {
            $this->called = true;
            return true;
        }
    };

    $action = new UserRegistration($driver);
    $action->register($username, $favoriteColor, $emailAddress);

    $this->assertFalse($driver->called);
}

Mock

Mocks are a special kind of test double that have an expectation about a method being called (or not), and can also have expectations on the arguments sent to that method. If any of the expectations are not met, the Mock will throw an exception, causing the test to fail. One big difference you’ll notice when writing tests with Mocks is you don’t have any assertions in the test, you have expectations on the Mocks.

We’ll use the same code from the Spy example to demonstrate Mocks. In this example, the Mock will validate the User arguments in the save() method, and then use the destruct() method to throw the exception if the save() method was never called.

/** @test */
public function registerSendsUserToPersistence()
{
    $username      = "j.doe";
    $favoriteColor = 'blue';
    $emailAddress  = 'jdoe@example.com';

    $driver = new class($username, $favoriteColor, $emailAddress) implements
        UserPersistenceDriver
    {
        public $called = false;

        public $username;
        public $favoriteColor;
        public $emailAddress;

        public function __construct($username,
                                    $favoriteColor,
                                    $emailAddress)
        {
            $this->username      = $username;
            $this->emailAddress  = $emailAddress;
            $this->favoriteColor = $favoriteColor;
        }

        public function save(User $user): bool
        {
            $this->called = true;
            if ($user->username !== $this->username) {
                throw new Exception("Expected Username: {$this->username}, found: {$user->username}");
            }
            if ($user->emailAddress !== $this->emailAddress) {
                throw new Exception("Expected Email Address: {$this->emailAddress}, found: {$user->emailAddress}");
            }
            if ($user->favoriteColor !== $this->favoriteColor) {
                throw new Exception("Expected Favorite Color: {$this->favoriteColor}, found: {$user->favoriteColor}");
            }
            return true;
        }

        public function __destruct()
        {
            if ($this->called !== true) {
                throw new Exception('Save method was not called');
            }
        }
    };

    $action = new UserRegistration($driver);
    $action->register($username, $favoriteColor, $emailAddress);
}

Testing that a method is never called is a little easier with a Mock. Here’s the test to ensure the Persistence doesn’t happen for the invalid User.

/** @test */
public function registerDoesNotPersistUserWhenInvalidColor()
{
    $username      = "j.doe";
    $favoriteColor = 'rainbow';
    $emailAddress  = 'jdoe@example.com';

    $driver = new class implements UserPersistenceDriver
    {
        public $called = false;

        public function save(User $user): bool
        {
            throw new Exception('save method was called');
        }
    };

    $action = new UserRegistration($driver);
    $action->register($username, $favoriteColor, $emailAddress);
}

Fake

The final type of Test Double is a Fake – this is an implementation of the dependency that has all the same methods as the actual implementation, but wouldn’t actually be used in Production. A great example of this is SQLite – great for testing locally and in CI environments, but you’d probably opt for a more robust RDBMS in production. Fakes are also most commonly used in integrated testing, rather than in isolated unit tests.

Fakes can also be a great choice if you have a dependency that’s been declared as final that does not use an interface, thus preventing you from extending it to create another kind of Test Double (this is a big red flag that your system is not going to handle change well, but that’s another blog post). In these situations, you’ll typically create the Fake, and use override configuration to make your tests use the Fake instead. The exact approach you take is highly dependent upon your codebase and/or application framework. Common applications for Fakes might include logging, databases, or sending e-mail.

Super Important Note: If you create a Fake for a class which does not have an interface, and/or is final, you run the risk of the test double getting out of sync with the actual contract. This is dangerous, and you should create an interface if at all possible!

On to our example… Let’s look back at our Chat Availability with the Presence Service example. If we want to do any integrated tests that need to reply on Chat being Available, but we can’t or don’t want to hit the actual Presence service or a sandbox version of it, we can create a Fake of the Presence Service, configure our application to use the Fake for the integrated tests, and not have to worry about accessing the real service.

Let’s assume the third-party Presence Service has provided us a Final class as part of their library, and this is what we’ll model our Fake after.

final class PresenceService
{
    public function agentsPresent(): bool
    {
        // Does some stuff on their server
        // to tell us if anyone is present
    }

    public function isAgentPresent(int $userId): bool
    {
        // Does some stuff on their server
        // to tell us if this person is present
    }
}

We can create a Fake that allows us to define when we instantiate it which users are considered present:

class PresenceServiceFake
{
    private $agents;

    public function __construct(array $agents)
    {
        $this->agents = $agents;
    }

    public function agentsPresent(): bool
    {
        return (count($this->agents) > 0);
    }

    public function isAgentPresent(int $userId): bool
    {
        return (in_array($userId, $this->agents) === true);
    }
}

Now we can write an integrated test that actually uses the entire system, but substitute the Fake. Again, how exactly you do this is going to depend entirely on your framework and/or service configuration, service container, dependency injection, etc.

class StartChatEndpointTest extends \PHPUnit\Framework\TestCase
{
    public function setUp()
    {
        $agents = [1, 2, 5];
        $fake   = new PresenceServiceFake($agents);
        /**
         * Configure your system to use the Fake
         * This is sample pseudo-code.
         */
        app::register(PresenceService::class, $fake);
    }

    /** @test */
    public function canStartChat()
    {
        $client   = new \GuzzleHttp\Client();
        $data     = json_encode(['message' => 'Hello!']);
        $response =
            $client->post('http://localhost/api/v1/chat',
                          ['json' => $data]
                          );

        $this->assertEquals($response->getStatusCode(), 201);
    }
}

Summary

In the majority of cases, you should take advantage of PHPUnit’s built in mocking tool, or another mocking library, but if you should ever need to create your own test doubles, you should now have a good idea of what each type is for, and how to create them from scratch.

Knowing which kind of test double you’re using can help you understand the purpose of your test – remember, Dummies and Stubs are often used in correctness and contract type unit tests, Spies and Mocks are used most often in collaboration unit tests, and Fakes tend to be used in integrated testing.

Advertisement

4 thoughts on “The Five Types of Test Doubles & How to Create Them in PHPUnit

  1. Pingback: PHP-Дайджест № 141 (1 – 15 октября 2018) - My Blog

  2. Pingback: Jessica Mauerhan: The Five Types of Test Doubles & How to Create Them in PHPUnit - webdev.am

  3. Pingback: PHP Annotated Monthly – November 2018 | PhpStorm Blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s