Don’t Give Up on TDD Before Reaching Your Breakthrough 

 January 22, 2019

by Jon Reid


Dan Abramov recently tweeted:

That's the beginning of a thread, so there's more to it. But it gives me a chance to say to Dan and the many folks who've shared this: You can still grow in test-driven development (TDD). (Heck, I'm still growing.)

But instead of short tweets commenting on other short tweets, I thought I'd reply here. There's room to discuss the gray areas.

TDD Lives in a System of Design

First, let me commend Dan, and my friends who shared his tweet: You've tried test-driven development. More than tried it… you've given it a decent shake. Way to go!

But after the initial rush of success, TDD can become hard. This is where I don't want you to give up. Why is it hard? Because TDD isn't a programming technique that lives in isolation. It's part of a broader ecosystem. To do TDD well, you need:

  • To design testable code. Much of this boils down to the SOLID principles, and other principles like them.
  • To express requirements in code. Each test lays down a new constraint. There's an art to shaping these constraints so that they triangulate to a solution.

I guess I'm talking about design—the design of production code, and the design of test code. Technically, these aren't part of the red-green-refactor "TDD Waltz.” But practically, your TDD won't get far without design skills. In the earlier days of TDD, there was discussion that TDD ought to stand for test-driven Design.

TDD depends on having fast, executable tests. Most programmers add "writing tests" onto an existing base of "programming," so the "test" part is new to them. This makes the tests stand out. It's easy to think of it as a testing discipline.

But it's not. Test-driven is the adjective. The noun—the root of the matter—is Development. And to do TDD well, we need to get better at Design. (See Your TDD Will Improve as Your Design Sense Improves.)

Lego blocks

TDD Lives in a System of Feedback

Much of the Twitter discussion was around the question, "Can't you use TDD but build the wrong thing?" And there were several responses along these lines:

  • TDD is good for some code (like libraries), not for others (like UI).
  • TDD forces you into linear thinking. So to non-linear thinkers, it feels constraining.
  • So TDD is one approach among many. It's not for all programming, nor is it for all programmers.

I'm not shaming anyone, especially my dear friends! That's why I'm replying long-form, instead of in a soulless tweet. Let's turn these into questions:

Is there room for play within TDD? Is there any place for non-linear exploration?

Again, TDD is part of a broader ecosystem. But this time, let's not talk about design so much. (Though if you find TDD hard for UI, it shows there's room to improve the design.) Instead, let's talk about the roots of TDD.

Test-driven development is part of a larger approach called Extreme Programming (XP). XP is a set of values, principles, and rules. "Feedback" shows up in all three. Here's a diagram of the most common feedback loops of XP:

Extreme Programming feedback loops

This isn't an exhaustive diagram. The "extreme" part of Extreme Programming says, what if we take a good idea (like feedback) and turn it up all the way? How can we get more feedback, to adjust our course as we go?

So where is play / experimentation / non-linear exploration? The answer is: these are all important forms of feedback. They find their place in a technique called Spike Solutions. I've written about Spike Solutions before; see How to TDD the Unknown with a Spike Solution. But let me summarize what they are, and how to use them.

A Spike Solution is old-fashioned hacking. It's noodling and tweaking. In TDD, it comes before the TDD Waltz. There are important questions we need to answer:

  • Are we building the right thing?
  • Heck, how do I even build this thing?

Some of this shouldn't even need programming, right? Non-code prototypes are a cheaper way of experimenting with UX and UI. But as iOS developers, we're also faced with many complex libraries that fit together. Quite often, we need to play around in code to get answers.

But here's the kicker: The final step of a Spike Solution is to throw it away.

Why Throw Away Code before Starting TDD?

The goal of a Spike Solution is to answer a question. Part of the discipline of spiking is to get that answer as fast as possible. This means the code can be uuugly. For example, I might sprinkle print() statements around.

But then, throw it away. Copy what you learned into a scratchpad for reference. You can keep the experiment on a temporary branch. But once you learn something, go back to the tip of your main branch and start over. (See my conclusion to Spike Solutions: 7 Techniques You Can Use.)

Why not keep the code? Because it's written without tests. I'm often surprised at the differences that appear when I start over. That's because TDD isn't just writing tests first. It's also emergent design. That is, the design emerges gradually, in response to the forces around it.

Test-after code has shortcomings that impact our agility:

  • It's easy to miss a test, leaving some untested code in place. You end up with a safety net that won't catch refactoring errors. And refactoring is essential for adapting old code to handle new circumstances.
  • The tests haven't exerted any influence on the design. You end up with designs that remain hard to test. And lack of pressure to write SOLID code leads to brittle code that resists change.

Don't Give Up… Please Share Your Experiences

So, what code do I not TDD? There are still qualities that need human assessment. Is it beautiful? Is it smooth? If we can capture any artifacts from these assessments, we may be able to get a snapshot: "It should look like this." Using these snapshots as Characterization Tests, we can still refactor the code.

But otherwise, doing TDD isn't a question for me. I don't always succeed, but those times become fewer and fewer. When you hit a roadblock to TDD, my encouragement to you is: don't give up. Not right away. Give your wild hacking creativity a chance to explore using the Spike Solution technique. Learn more about design principles. Reach out for help.

When you hit a roadblock to TDD, don't give up. Not right away.

Click to Tweet

I'm here to help you and your team succeed with TDD. Invite me to come teach my TDD for iOS Workshop. I can also help you through the challenges you face in your code.

Where has TDD not worked for you? Please share in the comments below. I welcome your thoughts and experiences.

Jon Reid

About the author

Programming was fun when I was a kid. But working in Silicon Valley, I saw poor code lead to fear, with real human costs. Looking for ways to make my life better, I learned about Extreme Programming, including unit testing, test-driven development (TDD), and refactoring. Programming became fun again! I've now been doing TDD in Apple environments for 20 years. I'm committed to software crafting as a discipline, hoping we can all reach greater effectiveness and joy. Now a coach with Industrial Logic!

  • Great article…I especially like the advice to “throw away” the results of your spike investigation.

    Adding my $0.02 worth:

    One of the biggest arguments I hear from my team against TDD is that it’s “too hard”. While I disagree, one of the biggest reasons for that statement is that we have a bunch of legacy (non-tested) code that you cannot easily even instantiate in a test suite – much less actually test.

    I strongly believe that instantiation is, itself, a test. How do I create an instance of my system under test? What inputs do I need? How I create/mock/stub them? How can I inject behavior or functionality?

    If you can answer those questions upfront and provide a way to just CREATE your SUT, then everything thereafter flows so much easier. An instantiation test (among other things) just reduces the number of barriers for the NEXT test to be written.

    Keep up the great work, Jon!

    • Hi Fred! You hit a big nail on the head. I think people expect TDD to be a technique they can bolt on to the way they currently code. It’s startling to be told, “This design is bad,” when that style has been working for you your whole career so far.

      When I add tests to legacy code, the first test is the hardest. And at first, all I’m trying to do is instantiate the dumb thing. It’s a battle. The good news is that once you get there, you’ve made it through the hardest part. The following tests are so much easier!

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}