TDD approaches
The Test Driven Development methodology is based on a relatively small set of rules or principles. But an aspect that isn’t explicitly defined is the way in which this can be applied to different development situations.
Thus, for example, the way in which we can direct the development of a class or a function through tests is very evident. A good deal of the kata in this book and, in general, the entry-level TDD kata in general, do exactly that. The problem arrives when we jump to the real world, a point at which many people fail to find benefits in the introduction of TDD in their development process.
The key issue here is that a user story doesn’t usually consist of developing a class and integrating it within the existing code, but rather, the usual is developing features that involve a set of components including some kind of interface to the outside world (UI, API), as well as use cases, entities and domain services, among others.
This leads to a very simple question: where to start?
The different ways of answering this questions could be reduced to three, not as distant between them as one might think. In fact, they’re not mutually exclusive.
Classic TDD or Detroit School
This approach goes by both names because it its, so to speak, the TDD original model propose by the founders of the Extreme Programming paradigm (Kent Beck, Ward Cunningham, Ron Jeffries), born in the context of the Chrysler Comprehensive Compensation System project in Detroit.
Following this philosophy, a complex project would usually be approached by defining the necessary software units and creating each one of them through a standard TDD process.
Taking a very simplistic example: imagine that our task is designing an API endpoint.
This would mean creating, at least, a controller, a use case, one or two entities, and their corresponding repositories.
In this classic TDD approach, one the necessary components have been determined, we would begin creating them in dependency order, starting from the domain entities and advancing outwards. That is to say, if to build a unit I need another unit, I will build the latter first. Since the dependencies point to the domain, it would be appropriate to start solving the problem in the domain layer and advance by going out towards the outermost layers.
Some of the features that characterize this model are:
- It’s tested against the units’ public APIs, using black box testing. This implies that we don’t make assumptions in the test about the way the unit is implemented.
- Special emphasis on the refactoring phase, in which the design is introduced. We must refactor as soon as we have green tests, as small as the opportunity may look.
- Keeps the use of test doubles to a minimum, essentially limiting them to architectural boundaries.
- Development goes from the inside and outwards. Prioritizes the identification and development of domain logic.
- It focuses on the state and outcomes of objects and their methods.
This approach provides TDD’s expected benefits:
- Work in small and manageable increments.
- Generate a safety net with many regression tests.
- Possibility of refactoring the implementation with great safety.
As for drawbacks, it should be noted:
- Tests don’t really help drive the design, but rather the implementation of the units. The design is done during the refactoring phase and can lead to the extraction of unit collaborators that are tested through the unit’s public interface.
- We run the risk of creating software units that are too large, something that can be addressed by applying refactoring intensively, specially extracting to private methods and collaborators when possible.
- Also, we run the risk of creating unnecessary functionality in the innermost units by not being clear about the requirements of the components that depend on them. It contradicts a bit the principle of interface segregation, which precisely promotes that they are defined by their consumers’ needs.
- Problems may arise when integrating the components.
Outside-in, London School or mockist
It’s origin also lies within the extreme programming community, but in this case, the Londoner one. It owes its name to the fact that it favors a methodology based on starting from the needs of the consumers of a system.
In general, the outside-in methodology states that a complex project would be approached by defining its outermost interface and working inwards, discovering and defining the necessary units on the way with the help of doubles.
Some features that characterize this model are:
- The interactions between the units are tested, also called white box testing. That is, the assertions verify the messages that some components send to others.
- The refactoring phase is less important, and the design is done while tests are red.
- Test doubles are heavily used, we have to decide which collaborators manage a unit at each moment, and doubles are created in order to discover and establish their interfaces. Real classes are implemented subsequently using a classic TDD process in which the dependencies are doubled first and implemented later. For this reason it’s also known as Mockist TDD.
- Development goes from the outside and inward, protected by an acceptance test.
- It focuses on communication between objects, so it may even be considered more of an OOP approach, in Alan Kay’s original sense.
Benefits:
- It provides us with a work approach that fits specially well in multidisciplinary teams and is more business-oriented.
- Reduces or eliminates the final product’s integration problems.
- Lowers the chance of writing unnecessary code, the interfaces are more compact.
- Introduces consideration for design early on in the development process.
- We pay more attention to interactions between objects. Having to use doubles first in order to design their interfaces helps us make them more concise and easy to manage.
- Fits Behavior Driven Development very well.
Drawbacks:
- The refactoring cost is higher because of its focus on interactions, and the tests tend to be more fragile due to their coupling to the implementation. However, we have to think that these interactions are necessary, and above all, they have been designed and decided by us, so they’re reasonably stable implementations.
Behavior Driven Development
It could be said that if we begin outside-in development from a more external step, we arrive at Behavior Driven Development.
In its main two schools, TDD is a methodology focused on the technical process of developing software. But BDD goes a step further by integrating business into development.
Schematically it’s still TDD. It begins with a test and the development is driven by new tests. The difference is that in BDD we ask ourselves about behaviors or features in which we’re interested, and we describe them in business language through examples. In fact, there’s a structured language with this very same purpose: Gherkin.
These descriptions are translated in the shape of acceptance tests and are developed from there, through a methodology that’s quite similar to outside-in which, in turn, can use TDD’s classic approach when it’s time to implement the specific software units. All in all, the kind of unit tests that BDD favors tend to use a “specification through examples” style as opposed to assertions.
In practice, BDD is outside-in TDD but taking the people that are interested in the software and their needs as a starting point, not the contracts or the implementation’s technical requirements.
There exist specific tools for this approach, the best known being Cucumber, in Ruby, which has ports for other languages. These tools are used to convert Gherkin documents into executable tests. But from this point on, we enter outside-in methodology.
So, what approach should we follow? And how do we learn TDD under the light of these approaches?
As mentioned at the beginning of the chapter, learning TDD through kata can be difficult to transfer to everyday practice in a real development problem. However, it’s a necessary learning before entering the outside-in approach, which is much more realistic in several respects.
Outside-in doesn’t exclude the classical approach, but rather puts it in context while providing us with a design focus driving by test to which you could roughly apply the same principles of TDD: start with a test, write minimal production code to make it pass, and refactor the solution if there’s an opportunity.
After all these are tools, and their point is to have them lying around nearby in order to use them when they come in handy. In real work, I’d say the important thing is to be able to mix styles conveniently. In a specific task we may start from a classic style, but after reaching a certain point we might introduce Mocks so as to not lose focus from a certain flow and be able to sort out the details later.
It’s harder to find kata in which to use an outside-in approach. In general they are longer and more complex, although it’s also possible to adapt some of the classic kata in order to practice it.
A TDD training plan could be structured as follows:
- Introductory training with classic kata
- Advanced training with kata in the form of agile-kata
- Outside-in kata
- Advanced training with complex agile-kata
References
- Does TDD lead to good design1
- A case for Outside-in Development2
- Detroit School TDD3
- London school TDD4
- Extreme programming: origins5
- The failures of “intro to TDD”6
- Endo-Testing: Unit Testing with Mock Objects (PDF)7
- The London School of Test Driven Development8
- Outside-In development with Double Loop TDD9
- “Tell, Don’t Ask” Object Oriented Design10