How to transition to Test-Driven Development (TDD)
The biggest challenge when transitioning to TDD is to try to abandon working with large batches (this is in my personal experience; others…
The biggest challenge when transitioning to TDD is to try to abandon working with large batches (this is in my personal experience; others may disagree). As I was struggling to learn how to do TDD properly, I gradually started to realize that everything is flowing much more smoothly if I agree to be more humble and instead of heroically grabbing the biggest chunk of a challenge, go for a tiny little piece of a challenge first.
I then realized that the smallest possible batch I could come up with is a single expectation. I expect that the system I’m working on will exhibit certain behaviour, but right now that behaviour is not there. For example, the ability to calculate and apply sales tax on the order total. The system presently knows how to reliably tally up the order total from the order line items, but it doesn’t know how to add sales tax.
So, that’s the expectation I’m starting from. At this point I cannot possibly imagine a smaller one. That’s the smallest batch I can come up with. I then codify that expectation in a microtest. I watch that test fail (the behaviour is not there yet), then implement the behaviour using the easiest possible code (by the easiest possible code I mean code that only uses primitive types and crude procedural conditional logic and other anti-patterns).
Why go with the easiest possible code?
The reason is simple: speed. It is much quicker for me to cobble up some half-baked spaghetti code than it is to sit down and try to carefully craft some code that would be at a high level of abstraction. I know from my previous experiences that I am not very good when it comes to crafting generic, highly abstract code by myself.
Why the need for speed?
The reason is simple: feedback. As I’m writing code, I get very little feedback. Why? Code is text, and computers don’t understand text. For me to verify if the code I wrote works as expected, I need to compile the code, build the app, configure the app, run the app, sign in to the app using some test credentials, navigate the app, then type in some test data. That’s a lot of work, but before I do it, I cannot get any feedback regarding the code I wrote. And as I keep changing the code, I also don’t get any feedback. I am taking shots in the dark, and that’s not making me feel comfortable.
But if I write the consumer of the code before I write the code (i.e., I write a microtest), that consumer can now give me much quicker feedback. Instead of me doing all the manual chores, the consumer of the code (the test script) does it all automatically for me. It really speeds up the process and I receive a quicker feedback.
Why do I need a quicker feedback?
That’s a good question. Theoretically speaking, if I am really a good programmer, I should be able to understand what the code is going to do by just reading it. When I was younger and less experienced, that is exactly how I understood programming. If you know how to program, you should be able to understand what the program is doing by simply reading the code that you, or someone else, wrote.
That’s a nice ideal, but in practice I’ve witnesses that it is more complicated than that. And it does not only apply to my limited intellectual capabilities. I’ve witnessed many of my colleagues (some of whom are way more advanced than I am) being forced to start a debugging session because they couldn’t understand how the program works by merely reading the source code. It was only by placing a break point in the code and stepping through it line by line, examining the produced values at each step, that they could finally understand what the program is doing.
So, there seems to be a serious cognitive dissonance between reading the source code and understanding what that source code is doing once it gets executed.
It is for that reason that I crave quick feedback.
Now that I get a quick feedback, what’s next?
Recall how I was rushing to make easy changes to the system just so that I could get some feedback — are the changes I made working as expected or not? I need to know that. I really do. Why? Because in my previous experiences, when I was typing reams and reams of code being convinced that everything will work just as I intend it to, I was almost always proven wrong. Discovering and acknowledging that pattern of always being wrong in assuming I know what I’m doing helped me change the error of my ways, and now I’m proceeding in tiny steps only. I write code gingerly.
Each time I make a diff to the source code, I need to learn whether that diff broke something. If it didn’t break anything (i.e., all tests still pass), that means that the diff is non-destructive — the system continues to work as expected.
If the diff breaks something (i.e., if at least one test fails), that means the system stopped working as expected and I need to revert to the most recent healthy state of the system. To do that, I nuke that latest diff and start all over again.
As I’m iterating in the above fashion, my intention is to clean up the code structure. By rushing to obtain feedback as soon as possible, I initially cobbled up the first draft of the shipping code. I knew that the structure of that first draft is really poor, but I made peace with that fact. The reason I agreed to write substandard code (despite me knowing how to write better code) lies in the fact that it’s the quickest way to get to the point where I can obtain feedback. Maybe my hastily cobbled diff made the test pass and the system works as expected. Maybe the test still fails after exercising my latest diff. In either case, I am satisfied because I received much needed feedback.
As soon as I get the feedback that the test passes, I turn my attention to cleaning up the mess I created with my first draft. I look for ways to eliminate code duplication. Each time I make a diff to inch my way toward the minimal or no code duplication, I exercise the system — remember, I crave feedback. I cannot know for sure whether my diff is working or not. I need a second opinion on that, so I listen to the tests.
I also like to eliminate the primitive obsession in my code. To make things work quickly, I initially resorted to only using primitive types: strings, ints, doubles, bools, arrays, etc. Those are not very expressive, as they do not talk in terms of the domain. I therefore iterate in search of more appropriate abstractions. In the process, I do a lot of ‘extract method’ type of work (and of course, many other tricks of the trade; a topic for another article).
In the end, I deliver shipping code that is very generic, speaks only in terms of business domain, all entities are properly named (i.e., understandable to the team working on the project), and the code reads like a nice short story.
With the refactoring done (for now), I turn my attention to the next expectation. In this example, maybe my next expectation is calculating the shipping cost?
Sky is the limit. As we progress carefully on our software creation endeavour, our system is up and working as expected every step of the way. And every step of the way we are ready to ship it!