Due to errors in communication between us and the clients, or in our teams, unforeseen circumstances in which our applications can and will be used by the end users, negligence, or purely due to lack of knowledge and skills, hardly any software we write is 100% free of any issues.
An industry standard in increasing the quality of the software we write is the use of unit testing, a method of testing software that focuses on a single unit of our application and verifies that the unit is working correctly under different circumstances.
These circumstances can vary from verifying that the output of these units is correct after providing them with different inputs, or making sure that the units can handle different scenarios during their actual production use, like missing database records, or unreachable 3rd party HTTP APIs.
A number of studies have been conducted and published on the positive effects of automated unit testing on software quality:
These unit tests, when automated, are the basis of a software development process called Test Driven Development (TDD). With TDD we first write the tests for our software, then we run the complete test suite to make sure that the new tests fail, followed by writing just enough code to make those tests pass. Then we repeat this entire process until the feature we’re implementing is complete.
This approach to software development has the advantage that the development cycle is shortened and the bugs that come from misunderstanding requirements or from programmer errors are caught early.
TDD also tends to drive the programmer towards cleaner code and the usage of design patterns, because code that is made easy to test is also easy to understand by other programmers and maintain in the future.
What the actual units of these tests are is usually up for debate, but in the case of Object Oriented Programming (OOP), the unit is most often a single class in our code base. Other units could be methods in our classes or even a group of classes that form a single module. For the sake of this article, we will assume that when we are talking about a “unit”, we mean a single class.
When developing applications using the TDD process there are two schools, and two approaches, we can take when it comes to writing our code. The first school, the older one, is the school of classical TDD, or the Chicago style TDD. The other one, the newer one, is the school of mockist TDD, or the London style TDD.
Both of these schools have their advantages and disadvantages and which one is used is pretty much up to the developer, or the team to decide. They can also be mixed; it is not unheard of to use one style in developing one part of the application and the other one in other parts. We should always use the right tool for the right job, after all.
When going with the classical TDD approach the code is usually developed from the inside out. These tests are good when we know in advance what are the classes and their methods and how they integrate with each other. It allows us to focus on one thing at a time, on the actual unit that is being developed. The tests verify the state of the unit after the tests were run and not on the communication between different objects that are used within the unit.
The usage of mock objects in the classical TDD approach is usually frowned upon and when the unit being tested requires a dependency, the actual implementation of that dependency is being used in the test. This requires us to write the innermost dependency first and then branch out from there, hence the “inside out” approach.
The mockist TDD approach allows us to develop our code from the outside in. These tests are good when we want to take a “discovery” path down our code base. The dependencies of the unit being tested are “mocked out” — -mock objects are created that mimic the behavior of the real dependency.
The actual implementation of these dependencies that we are mocking can be written at a later time. This leaves us with the opportunity to start developing from the outermost layer and work our way in, discovering the API of our dependencies, hence the “outside in” approach.
Both classical TDD and mockist TDD have their place in the software development process and their strengths and weaknesses must be considered when we choose what approach are we going to take when working on a particular piece of the application.
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 nowhere to be found?Read more