TDD for API Discovery
Testable systems ensure adaptability for future changes.
Yet, having a testable system does not mean the design is robust. It depends on the TDD practice knowledge of the code creator. The primary benefit of TDD is not adding more tests for the sake of it. The focus is on breaking down the problem into manageable pieces that can be analyzed.
The essential advantage is that the design can only improve by creating more tests.
A better design can be measured by comparing two solutions to the same problem. It is wrong to assume that a system has a better design because it has unit tests. TDD leads to a better design by placing the minor code based on examples. There is no room for speculation.
What we write depends on the example we define.
To understand what a “better design” means, we must first define it. This will vary depending on the problem, but at least we can determine what a “good design” is. A good design should always simplify.
There is a close connection between good design and velocity.
If the team’s speed cannot be measured or does not exist, there may be issues with the test suite. If the team values testing, they have a test suite that increases its running time per application feature. This is the suggestion presented by the Test Pyramid. We want to have as many unit tests as possible and reduce the number of tests. These two things equate to a better design and more accessible-to-unit test systems.
We make design decisions every time we write code, whether using TDD or not.
If the application succeeds, many of today’s decisions may need to be changed later. This is not because of the lack of diligence by the developers but because it is challenging to test an untested system. Yet, the resistance that someone may feel towards testing something is more related to poor design than TDD.
For example, a common issue is having a chain of mocks that developers tend to stub to isolate the database.
Yet, there is a violation of the Law of Demeter, and there are more effective solutions than stubbing everything. Getting rid of this issue before moving forward with stubbing the world is vital. The fallacy here is not that tests do not lead to better design; they lead to better design because the code is testable from the beginning. This is the heart of the fallacy I want to address because I have seen many codebases where this is not true.
The code could be better designed because it is more testable.
We must discard any pre-made patterns or assumptions about the future to have the flexibility to change. Good design always moves towards simplification. Facing a particular pattern creates more friction when a new change is requested.
A communication pattern refers to guidelines dictating how a group of objects interact. These patterns are only recognizable within a particular context.
Teams must establish these patterns, which is where TDD proves beneficial.
To better define the behavior of objects, their roles in the system should be considered. These roles can be grouped through interfaces. Additionally, it is advisable for the thing containing the data to exhibit the appropriate behavior. A practical way to achieve this is to avoid incorporating getters and setters as part of objects.
Our goal is to represent the collaboration between two objects. This allows us to discover the interface without creating the actual object.
Interface discovery is a technique that defines an API based on the consumer rather than the provider. We create mock collaborators, define expectations for each one, and check them in tests. We can identify the provided interface by specifying what each side needs.
To discover interfaces, follow these steps:
- Create any necessary mock objects.
- Create any require objects, including the target object.
- Specify how the target object should call the mock objects.
- Call the triggering method(s) on the target object.
- Assert that any resulting values are valid and that all expected calls have been made.
We iterate between the target object and its environment by following these steps. This process results in the definition of an API that supports the system.