In Test-Driven development, it is considered a best practice to write a test for the code you wish you had before writing the actual code. In that spirit, here is an overview of the TDD practice I wish I had. In other words, while the way I currently build apps would not be considered TDD, it is certainly something I aspire to, because I very much believe in its power.
TDD in a nutshell
For those unfamiliar with TDD, or Test-Driven Development, very briefly, it is a model in which you first write a code-based specification of what some part of your system should be able to do and what result you expect when it is done. After that, you run the test and the test fails, because there is no code that does what was expected. Then, you write the least amount of code needed to make that test pass.
A single test has little value; the power of TDD is in numbers.
What’s the big deal with regression?
Of the many benefits of TDD, a huge one is that you can push code to production and still sleep at night.
Let’s say I’m working on a banking system, and I add a feature for filtering a list of accounts based on if there have been any overdrafts in the last n months. But in adding this filter, I unintentionally modify the overall filtering of accounts, such that accounts with a zero balance do not appear at all in the list. Since we don’t have a test suite that checks for this, the feature ships and everything is hunky dorey until a couple days later when users of the system realize they suddenly can’t view brand new accounts with a zero balance, and all hell breaks loose.
Now multiply this issue by ten or a hundred and you start to realize the type pain that TDD can prevent.
Believe me, you do not want to be in this situation. Of the many benefits of TDD, a huge one is that you can push code to production and still sleep at night.
Ok, enough introduction. Let’s get back to the main topic: what I would consider an ideal TDD process.
My Ideal TDD Process
These are what I would see as the main steps of an ideal TDD flow.
1. Wrap TDD in BDD
Make sure any TDD work is driven by an actual user need
In terms of process, this means that we begin by picking up a user story from a backlog and then convert that story into acceptance tests, which I would work with a PO, domain expert, or the like, to write.
2. Stub out unit tests in pseudo code
Start by writing tests in plain English
3. Convert each line of pseudocode into a test
Next, we turn a line of pseudocode into a code-based test. Very often this process of “translation” can lead to additional insights, eg maybe something was easy to write in plain English, but not so trivial, it turns out, when writing in actual code.
4. Get each test to pass before working on the next one
If you convert all your code stubs into actual tests you might waste a lot of time.
5. Keep going until the original acceptance tests pass
This is the essential workflow. We just keep going until the original acceptance tests pass.
Are we Done?
Once those test pass, after a quick sanity check of the acceptance tests (eg maybe the test is passing but there is something obviously wrong when manually completing the task), I would ask the Product Owner to bless the feature as Done.
Usually, there will be some tweaks needed to the feature and we will decide if they can be added later, in the form of a new user story, or if they must be added for the feature to be considered Done. One way or another, we will hopefully get to Done, and we can pick up the next story in the backlog and the process starts all over again.
What about refactoring?
A mantra of TDD is “Red, Green, Refactor” which means you write a failing test, get it to pass and then refactor (or revise) your code. Since you now have a test that checks if your code is working, you can refactor with confidence. (Remember my discussion earlier about regression?)
My preference is to combine refactoring with writing new code.
That is all good and well, but I am loathe to spending too much time refactoring for its own sake. Yes, if there is some obvious cleanup that can be made, by all means, do it. But overall, my preference is to combine refactoring with writing new code. In other words, the mantra becomes a kind of recursive Red/Refactor, Green, where every instance of writing new code is an opportunity to refactor old code.
I also wanted to mention two aspects of TDD that aren’t necessarily process-oriented but still an essential part of what I would consider an ideal practice.
Pairing and TDD
Pair programming is, of course, another staple of good Agile practice, and I am a huge fan of working this way. In fact, I would say that if you are doing pairing and TDD really well, you’d be hard-pressed not to ship code that is totally rock solid.
When you are pairing and doing TDD, alternate between writing tests and getting tests to pass.
And this gets to a key relationship between TDD and pairing, which I need to credit the good folks at Pivotal Labs for showing me: When you are pair programming and doing TDD, consider alternating between writing test code and writing the code that gets the test to pass. This will allow you to continually shift your perspective from looking at the system from the outside in and vice versa.
Integrate Visual Design into your testing
One of the most common oversights I see with testing in general is for it to be focused solely on code. But, as we all know, the visual display has a huge impact on usability and overall success. At the same time, it’s easy as a developer to lose sight of the UI and visual layer in general. To help prevent that from happening, one can build visual regression testing into the overall testing workflow, such as by using a tool like percy.io.
That, in a nutshell, is an ideal version of a TDD practice from my perspective. I’d love to hear your thoughts in the comments!