It is easy to be the proponent of the classical TDD or mockist TDD when we are starting to develop a new application, but what should we do when we inherit an application where the code is already written and the tests are no where to be found? My career as a software consultant gave me the opportunity to work on applications where design patterns, best practices, dependency injection, and everything else that makes a code base easy to work with are wishful thinking. The “easy” answer in those cases would be, of course, let’s rewrite the entire application from scratch. If an application enables a business to earn a living, has users using it daily, then a complete rewrite is rarely the correct answer, easy or not.
After a thorough investigation of the existing code base one of the first things we should do is to try and cover the existing functionality we need to work on with tests. Integration tests, unit tests, end-to-end tests, the more type of tests we can write, the better. For integration and end-to-end tests we should mostly focus on having a test database and a good set of fixture files, while for unit tests we’ll probably need to create test doubles for the dependencies. These tests should be a support for us while we are trying to learn, understand, and improve the existing code base, and those tests should change over time along with the code that we are improving.
But what are these test doubles? They are objects that, in testing, can replace the real objects that would otherwise be created and used during the real execution of the application. Using test doubles in our tests we can ensure the isolation of the dependencies from the unit under test, to mimic classes that we didn’t even write yet, to allow us to discover the APIs of our classes by exploring how would they interact without worrying about their implementation detail, as well as keep the test suite fast as calls to databases or HTTP endpoints are replaced with these test doubles.
The process of creating a test double is called “mocking” and there are cases when the term “mock object”, or “mock”, is used instead of the term “test double”, whereas the truth is that a mock object is only one type of a test double. The types of test doubles we can create are:
They all have their place in the world of unit testing, regardless if we are working on a green-field project applying the classical or the mockist TDD process, or if we are working on a legacy application that is difficult to test.
Dummy is a type of test double that is created only to be passed around, but its methods are never actually called by any of the code that we are testing. It can be created manually or with a mocking framework. It’s most often used to fulfill the argument list for a method call.
Fake is a test double that is always manually created and it’s a simplified implementation of the same API as the real “thing”. An example of a fake would be an in memory database.
Stub is a test double that is used in the place of a real object, when we only need the test double to return a predefined result so that the code under test can be brought into a working state. It can be created manually, but a mocking framework should be used to help speed up the process.
Mock is the “big brother” test double of a stub. Besides setting a predefined return value, we can also set up expectations how the methods on the mock object should be called, with what arguments, and in what order. Mocks are most often used in the mockist style of TDD, and whenever we are interested in how the unit we are testing interacts with its (mocked out) dependencies.
Spy is a type of test double that records the interactions between it and the unit that we are testing and allows us to verify the method calls we’re interested in at the end. This approach makes it possible for the unit test to follow more closely the Arrange-Act-Assert style of writing unit tests.
In PHP we have several libraries to help us create test doubles.
Prophecy is a framework for creating test doubles that was initially built for the requirements of phpspec, but it can be used with any other PHP testing framework. Since PHPUnit 4.5 it bundles Prophecy within PHPUnit itself, but as of PHPUnit 9.x this bundling is deprecated and set to be removed in PHPUnit 10.
Mockery is another framework for creating test doubles. It can be used with PHPUnit, phpspec, Behat, or any other testing framework. I find it especially powerful when working with legacy code, due to its support for creating partial mocks or mocking hard dependencies.
It is also possible to test without mocking frameworks, but still use some types of test doubles.
Even though test doubles are helpful when writing unit tests, we need to use them sparingly. Test doubles require additional maintenance, and if we overuse them the quality of our tests can decrease.
What’s your take on test doubles? Love ’em or hate ’em? Let me know in the comments.