Absolutely necessary, often abused, they come in different flavors that serve different purposes. They are the test doubles. Learn about them in this extensive guide.
The code samples in this post are written in PHP and reference some details of the language and its ecosystem, although the concepts should apply to most OOP programming languages.
What are test doubles?
Martin Fowler defined test doubles in a clear and straight to the point way:
Test Double is a generic term for any case where you replace a production object for testing purposes.
It is important to remark the any case: it applies not only to classes in unit tests. When we use a different database system when in a test environment, or when we use a mocked server instead of accessing an external API, we are using test doubles.
When we use test doubles?
The Subject Under Test (SUT) is the set of elements of our system we are testing at a certain moment. This set of elements define the boundaries of the SUT.
We use test doubles to replace the elements that interact with the world outside these boundaries.
It is of capital importance to correctly identify in each case the SUT, so we can know what we should replace with a double.
In this post, we will see examples of different scenarios and types of tests.
Why we do use test doubles?
When we write a test, we should only care about the behavior of the SUT. When replacing these elements interacting with the outside world, test doubles behave like the doubled elements but removing a big part of their complexity, keeping the subject under test encapsulated when exercising its logic and optimizing the feedback loop we get with the tests.
Imagine we are testing a service in charge of booking a table in a restaurant. Its behavior covers checking if there are tables available, creating the booking if so, and saving it in the database.
This booking service is our SUT and, in a unit test context, the behavior we want to test stops at saving it. We are interested in the fact that the booking is saved, not caring about the type of database, the number of tables involved, etc. aspects that are irrelevant for the service logic.
Using a test double in this scenario will allow us to assert the booking is saved without the complexity carried by a real database.
Not need to say the database is still important and it should be tested in another context, where the SUT covers the database inside its boundary.
Using test doubles
Usually, we relate test doubles to replacements of classes in unit tests. However, as mentioned before, a test double may replace anything that is in production code.
The SUT and the type of test we are dealing with are the key factors to understand when it is needed to use a test double, and the type of test double required.
Using tests doubles in functional tests
Functional tests are high-level tests where we exercise a big part of the application, from a big monolith to a bunch of services at once -services in this context referring to high-level services like independent applications.
In high level, functional tests, the SUT is usually a big part of our system, when not the whole system at all.
In this scenario we usually want to use a configuration as close to production as possible, maybe with real databases, file system, queues... elements we usually want to cut off in more granular tests using test doubles.
However, we do not want our tests to depend on elements that are completely out of our control, like external systems. This would cause tests failing when the external system is down for some reason, even if our application behaves as expected, plus some other nasty implications like allowing connection to the outside world from within our test environment.
To prevent this situation, we can double the whole external system using a stub server where we configure fixed responses for concrete request data. Obviously, the responses from our stubbed server must be exactly the same as the real one.
In the same manner, we can replace the email system to avoid sending emails to customers when running the test suite.
In a high-level context, test doubles give us control over elements we do not control in the production environment.
Using tests doubles in integration tests
Integration tests cover the interaction between different components of the system. This component can be a database, a call to some API through HTTP, some other module in our application,...
When doing integration tests, the SUT is defined by the communication between components. The key here is the communication, not necessarily the technology, system, or whatever that is on the other side of the wire.
Most likely we will interact with the database through some vendor library, let's say Doctrine, a well-known ORM in the PHP ecosystem. Doctrine is the other component in the integration test, the one that our component communicates with.
In this context, we don't care that much about the database system being used -at the end, that's why we use an abstraction layer over it-, so it is totally fine to replace it with another that performs better, for instance using SQLite instead of MySQL.
Another example: some class executes an HTTP request to an API. In this case, we cannot replace HTTP with another similar, more convenient technology. But we do not need a real service responding to the request, having a server with some predefined responses can be enough.
To take into account, we should test one kind of communication at a time. If we test communication with the database in a class that also puts messages in a queue and does HTTP requests, we must use test doubles for the elements that are not using the database. They will have their own test when database communication will be doubled.
Using tests doubles in unit tests
Unit tests cover the behavior of a single component of our application. Usually, this means from one to a bunch of classes in charge of performing a concrete action. That is the SUT in this context.
For the classes that act as a communication point with the world outside our SUT, we want to use test doubles instead.
To do so we need a way to make the replacement effective. The most common and desirable way of doing it is through dependency injection.
Usually, we find constructor dependency injection, so instead of injecting the regular dependency in our class we inject the test double:
final class MyClass
{
public function __construct(Logger $logger) {...}
}
final class MyClassTest extends TestCase
{
protected function setUp()
{
$this->logger = $this->buildLoggerDouble();
$this->myClass = new MyClass($this->logger);
}
public function testSomeLogic(): void
{
//Configure the logger double to behave in a certain way
//and exercise the logic of MyClass we want to test
}
}
Other types of dependency injection will work in a similar fashion.
In any case, one of the first steps to be able to do unit tests and use test doubles at class level is to extract its dependencies, so devote time to do so when you are not in such a situation.
Writing test doubles
Here we will be mostly referring to test doubles in the context of unit and integration tests.
We have some options when it comes to writing test doubles. The first decision is to use a mocking library or write the test doubles ourselves.
Mocking libraries give us total control over the test double. They create always a mock that will behave as one type or another of test double depending on the amount of configuration we add.
//Using PHPUnit built-in mock engine
$loggerMock = $this->createMock(Logger::class);
Using a mocking library produces test doubles fast and it can come very handy when code is not well designed. The downsides are that there is a tight coupling between the test and the doubled dependencies and also with the library itself. It is easy to do the mistake of testing implementation instead of behavior, because of the deep levels of configuration they allow, making more difficult to refactor and tests harder to follow.
When we choose to write the test doubles ourselves we need a way of coding a double of the same type as the replaced class. For that we can abstract the class methods involved in an interface or extend the original class overriding the constructor and the required methods:
interface Logger
{
public function log(string $someMessage): void;
}
class TheRealLogger implements Logger
{
public function log(string $someMessage): void
{
//Stuff involving database, filesystem, vendors,...
}
}
//Test double implementing the interface
final class MyDoubledLogger implements Logger
{
public function log(string $someMessage): void
{
//Do nothing, it is a test double!
}
}
//Test double extending the original class
final class MyDoubledLogger extends TheRealLogger
{
public function log(string $someMessage): void
{
//Do nothing, it is a test double!
}
}
If the collaborator we want to double is already implementing an interface, then it is a no brainer, we implement the double as a new instance of it. If there is no interface yet, we should evaluate which of the options is more desirable: go with inheritance with all its downsides or adding a new abstraction to maintain with a new interface.
Self-made test doubles are usually easier to understand than mocks generated by a library: they are simpler versions of the code we write on a regular basis and they stick to the same class public API as the real classes. They are also very friendly to IDE refactoring. The main downside is that they require extra time to be written compared to using mocking libraries, and sometimes they become complex enough to deserve their own tests.
I personally prefer to code my own test doubles as much as possible. They are not that time consuming and they come with some extra syntactic sugar that makes the tests easier to read, such as named constructors and explicit assertions. However, there are times that library mocks are more convenient. We will analyze the different situations in a moment.
Known the options, it is time to go through all the different types of test doubles.
The dummies
A dummy is the simplest test double possible. All the methods of the doubled class are nullified. That means they do nothing or the very minimum for the code to compile and do not break in runtime. We use dummies when we do not care about the usage of this dependency on the current test.
A logger is a usual example: we write tests to cover some logic where logging is irrelevant.
//Selfmade dummy
final class MyDoubledLogger implements Logger
{
public function log(string $someMessage): void
{
//Do nothing, it is a test double!
}
}
//A PHPUnit mock with no configuration acts as a dummy
$loggerMock = $this->createMock(Logger::class);
The stubs
When we want our test double to return a fixed result, we use a stub.
If our test double replaces a class doing an HTTP request and returning some value, with a stub we set up this value and the double will return it straight:
final class HttpExchangeRateProvider implements ExchangeRateProvider
{
public function getRateFor(string $currency): float
{
//Do a request
}
}
//Writing our own stub
final class StubbedExchangeRateProvider implements ExchangeRateProvider
{
private float $rate;
public function willReturnRate(float $rate): void
{
$this->rate = $rate;
}
public function getRateFor(string $currency): float
{
return $this->rate;
}
}
//Configuring a PHPUnit mock to act as a stub
$exchangeRate = $this->createMock(ExchangeRateProvider::class);
$exchangeRate
->method('getRateFor')
->willReturn($someRate);
The fakes
Fakes are smart guys that perform some actual logic to behave in a similar way as the doubled class, but this logic does not cover all the cases and cannot be used in production code.
A classic example is an in-memory repository that keeps copies of the elements added and is even able to find them:
// A fake made by us
final class InMemoryUserRepository implements UserRepository
{
public function add(User $user): void
{
$this->users[$user->id()->toString()] = $user;
}
public function ofId(UserId $id): User
{
return $this->users[$id->toString()];
}
}
//Configuring a PHPUnit mock to act as a fake starts to become verbose
$repository = $this->createMock(UserRepository::class);
$id = new UserId('some id');
$user = new User($id);
$repository
->method('ofId')
->with([$userId])
->willReturn($user);
Fakes can become complex enough to deserve their own tests. For instance, I usually write tests for my in-memory repositories.
The spies
Spies are test doubles that allow us to control how they are called. They track how their methods are invoked so we can do assertions based on their usage.
We use spies when it is important to assure they are used a certain number of times, with certain parameters, or both. For instance, if it is crucial for us that some action is executed just once because it is resource costly, or running it twice triggers some undesirable logic.
However, we must be cautious: intensive usage of spies may be a smell of tests focusing on implementation instead of behavior.
final class EmailSenderSpy implements EmailSender
{
private $emailsSent = 0;
public function send(Email $email): void
{
$this->emailsSent++;
}
public function numberOfEmailsSent(): int
{
return $this->emailsSent;
}
}
$emailSender = $this->createMock(EmailSender::class);
$emailSender
->expects($this->once()) //here we expect once, at least once, a concrete number...
->method('send');
Previous examples control the number of times the spy is used. Spies can also control the parameters provided when used:
final class EmailSenderSpy implements EmailSender
{
private $emails = [];
public function send(Email $email): void
{
$this->emails[] = $email;
}
public function usages(): int
{
return count($this->emails);
}
public function emailsSent(): array
{
return $this->emails;
}
}
$emailSender = $this->createMock(EmailSender::class);
$emailSender
->expects($this->once()) //here we expect once, at least once, a concrete number...
->method('send')
->with($someEmail);
With self made spies we have the possibility of adding some syntactic sugar to have more explicit and meaningful assertions, compared with the ones generated with a mocking library -we will get back to this when talking about mocks. With a library we set expectations in the arrange part of the test, but do nothing in the assert part because the expectations are implicit assertions. Consider the following ways of asserting the spy usage just adding some simple methods to our spy:
$this->emailSender = new EmailSenderSpy();
self::assertEquals(3, $this->emailSender->numberOfEmailsSent());
self::assertTrue($this->emailSender->sentANumberOfEmail(3));
self::assertTrue($this->emailSender->wasNotUsed());
self::assertTrue($this->emailSender->sentAnEmailTo('thor@asgard.com'));
The equivalent using a mocking library will require a more complex setup of the mock.
As always, we must consider when the plus in readability is worth the investment. Our spies may become too complex and start to do some error-prone juggling that may not be worth to implement compared to using a mocking library.
Mocks
Mocks are what mocking libraries provide. We cannot write them ourselves because we would be writing a mocking library for that. They are extensively configurable and based on how we set up them, they will act as a dummy, a stub, a fake or a spy, as we have seen.
The examples shown so far had little configuration, but things can become quite verbose when configuring more complex behavior. Overall, self-made doubles tend to be more straightforward than mocks in terms of readability.
Mocks may also allow to do some exotic configurations, like expecting to be called in a certain order with certain parameters in each case, evaluate parameters using closures, and so on. Although there are cases
when this may seem handy, treat them as possible smells of weak tests coupled to implementation.
Tips and tricks
Don't repeat yourself
It is very likely we will use the same test double with different configurations in different tests. Writing our own doubles implicitly allows re-usability, but mocking libraries do not. To avoid that, consider using factories for building your mocks.
This pattern will imply somehow linking the mocks created with the test case, so the mock expectations can be evaluated. The following is an example using PhpUnit:
//In a PhpUnit test case
$userRepositoryMockFactory = new UserRepositoryMockFactory($this);
$repositoryMock = $userRepositoryMockFactory->alwaysFindingUser($user);
Anonymous classes
A cheap way for using test doubles quickly is to use anonymous classes implementing an interface:
//a dummy using an anonymous class
$this->logger = new class implements Logger
{
public function log(string $someMessage): void
{
}
};
The downside here is it cannot be reused for other tests unless we move it to some mock factory.
I consider this approach a good one to write tests faster but, usually, I move the doubles to a separate class in a later step.
Self shunt
This technique consists in using the test case itself as a double, making it implement the interface:
class UserAuthenticatorTest extends TestCase implements UserCredentials
{
public function getUsername(): string
{
return $this->username
}
public function getPassword(): string
{
return $this->password
}
public function testItDoesNotAllowWrongUserPasswordPairs(): void
{
$this->password = 'an invalid password';
$authenticator = new UserAuthenticator();
self::assertFalse($authenticator->authenticateWithCredentials($this))
}
}
Same as for anonymous classes, it is a good technique to go faster at the beginning, but I prefer to move to separate doubles once the test is complete and passing. Self shunt may make a lot of sense at the moment of writing it, but for a developer without context -like yourself the day after- can be mind-blowing to see the test case passed as a dependency or parameter.
Adding sugar to our own doubles
As we have seen, we can spicy spies to improve the explicitness of the assertions and improve the readability. A similar technique with similar benefits is adding named constructors to our test doubles.
Compare the following test double for a validator:
class UserValidatorStub extends UserValidator
{
private bool $validationResult;
public function __construct(bool $validationResult)
{
$this->validationResult = $validationResult;
}
public function validate(UserData $userData): bool
{
return $this->validationResult;
}
public static function alwaysPassing(): self
{
return new self(true);
}
}
In the tests, we will use the static factory method to explicitly indicate how the validator will behave. In just one line. For a mock, it would be way more verbose and less readable.
$this->userValidator = UserValidatorStub::alwaysPassing();
//vs the way less explicit
$this->userValidator = $this->createMock(UserValidator::class);
$this->userValidator
->method('validate')
->willReturn(true);
Test doubles and code not that well designed
In poorly designed code it may not be easy to plug and play a test double. The boundaries of the SUT can be blurry due to questionable implementation decisions.
For instance, consider the sad reality of applications intensively using Active Record pattern all around. Active Record is a pattern that works well in a certain context, but a pain to unit test due to its usual implementation – fluent interfaces, mix of modeling and access to database.
Poorly designed code enforces us to do dirty things as an intermediate step to a better design. A key part of this progressive refactoring process is to extract dependencies and double them in tests. Until we realize what are the proper abstractions for dependencies, we can go all-in with mocking libraries, even with big, exotic configurations, as long as they make the work done and we are conscious we are moving to an intermediate state, settling the bases for a better and more testable design.
Another noticeable fact is that, in a messy codebase, we may not be certain if some collaborators of our SUT will eventually run logic outside its boundaries. A sad reality out there are apparently innocent value objects that subtly access all kinds of infrastructure elements. Again, it is perfectly valid to wildly mock as a step to a better design.
Final words
Test doubles are a core tool in testing and, like many other tools, misusing them will bring more pain than value. Hopefully, after reading this post you have a better idea of their whats, whys, and whens.
Don't hesitate to leave a comment if you agree, disagree or just want to add something. Feedback is more than welcome.
Top comments (0)