TDD is Not for Me

This will be the second time I’ve written about an unpopular opinion I have about testing, and I’m sure it will ruffle a few feathers. I don’t always practice Test Driven Development (also known as TDD), and I believe there are many situations where practicing TDD is more of a hinderance than a boon.

I’m going to explore why I don’t always practice TDD in a moment, but I want to be clear beforehand that this is not a TDD bashing article. In reality, I think learning about and trying TDD is incredibly beneficial for developers. TDD forces developers to think about development and design from another perspective; rather than focusing on, “How am I going to implement this?” they instead need to step back and think, “How would I use this function?” While this change may seem minor, it can result in drastic improvements to a codebase.

So why am I writing this article?

I am writing this article because there are many developers out there who struggle with TDD and feel poorly as a result. Whether it is imposter syndrome or just feeling like they have a dirty secret they need to hide, those feelings exist in far more developers than most people realize because everyone is too worried to admit that they just can’t make TDD work for them. This is made even worse when a developer hears others talking about how TDD is this amazing thing that made them so productive. In short, there are many coders out there who feel like crap and they shouldn’t; TDD is a great learning tool but I can say firsthand that it isn’t always effective, and in many cases it just hinders my ability to produce high quality code quickly.

Alright, now let’s dig into why I don’t practice TDD all the time.

TDD focuses too much on unit testing

Test driven development is often taught as a process with three steps: Red, Green, Refactor.

The idea here is pretty simple:

I tried to follow TDD pretty strictly for a bit, but at times it just felt like a massive pain; it kept slowing me down in every once in a while rather than helping me be more productive.

I couldn’t really put my finger on exactly why that was the case until I read this tweet by Jeffrey Way:

I’d wager it took me twice as long to become comfortable with TDD due to the way it is traditionally taught. 90% of everything I read focused on unit testing.

Jeffrey puts into words what I have been struggling with for quite some time; TDD is hard to learn and grasp when we focus so much on unit tests and completely ignore the more complex scenarios that every developer is bound to run into sooner or later.

In nearly every case where I find using TDD troublesome, it almost always stems from me trying to unit test some code where I need to mock out everything under the sun.

TDD is supposed to help us take a step back from the implementation and instead focus on how the code might be used, but when I am writing unit tests where a bunch of dependencies need mocked out that is no longer true. I am forced to once again start thinking about implementation details like, “Will my code need access to a database?” and “What about encoding? Do we need to support multiple output formats? Should I inject a mock encoder for the test?”

It just doesn’t feel natural to me to think about code this way. It isn’t beneficial to start thinking about what dependencies I’ll need before I even start to use them. Instead, I work better when I take a step back and write a feature test. Something like, “When I POST to the /orders path with the JSON {"price": 123, ...} I expect to get the following JSON back with an ID that matches the ord_[a-zA-Z0-9]{8,}$ pattern.”

Not only is this type of test incredibly easy to write - we just spin up an app, hit the endpoint, and check the results - but it is also gets me back into the correct mindset. I’m now thinking about how someone might use actually use the code; I’m thinking about another developer interacting with my API, or a real person filling out a form and submitting it.

There are obviously exceptions to this. For instance, if I’m writing a function to factor numbers TDD could lead to a reasonable solution to the problem. The key components here is that we aren’t really focusing on mocks and dependencies; we are instead writing an isolated function and TDD can shine in situations like these assuming we don’t fall into the second trap of thinking we have to write the absolute minimum amount of code at all times.

I’m also not saying you shouldn’t ever write unit tests where you mock things. These tests can provide value in many environments. I just don’t find myself practicing TDD as often when writing my unit tests. Instead, I unit test my feature tests and then write unit tests as I see fit.

Not all code should be written one test case at a time.

TDD is often taught using the following rules:

  1. You are not allowed to write any production code unless it is to make a failing unit test pass.
  2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
  3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

While this can work in many situations, I wholly disagree with the idea that all code should be written this way.

What I expect to happen here is for someone to link me a video, a blog, or some other example where the developer uses TDD to derive some complicated algorithm. One popular example is writing a function to determine the factors of a number. I’ve also seen articles where the author explores whether it is possible to derive something like quicksort via TDD.

Surely if these more complicated algorithms can be derived through TDD then it must work in all cases, right?

While TDD can be used at times to derive a reasonable algorithm, I have also seen countless instances where it has worked in the exact opposite way. By using TDD the developer derived an algorithm that was incredibly slow, inefficient, and sometimes even missed edge cases because it is nearly impossible to think of every edge case, let alone test them all.

The truth is, there are situations where you actually need to sit down and think about a little more than just expected inputs and outputs for a function. The easiest example to grasp is probably a sorting algorithm - while you might derive quicksort from a TDD approach, you could just as easily derive an algorithm that is far slower and less efficient. Even if you did come up with an efficient quicksort, most standard libraries use something a little more complex than this because it isn’t as efficient to use quicksort with smaller sized lists.

Going beyond algorithms, there are also times where constantly context switching just hurts overall productivity. While it might work well for many developers to constantly write one test, then one or two lines of production code, test again, refactor, then repeat, I personally find this constant context switching a distraction. I find that I am far more effective when I :

  1. Write a few test cases demonstrating the basic functionality I expect.
  2. Spend time to think about how I might achieve that functionality.
  3. Implement a rough version that gets my tests passing.
  4. Refactor as necessary.

This is pretty similar to TDD, but it isn’t quite the same and I’m sure if I taught it as my version of TDD many would tell me I’m “doing it wrong”. 🤷‍♂️

Wrapping up

I find TDD to be beneficial at times and I’m not saying we should abandon it. Instead, what I am trying to convey is that getting caught up in this strict set of rules defining what is and isn’t TDD is a mistake.

We should instead take the lessons we can learn from TDD and apply them in the way that is most effective for ourselves. If that means we end up breaking a few of the rules, so be it. After all, the goal of TDD, agile, or really any development process is to make us better at our job, and if they aren’t doing that then something needs to change.

Learn Web Development with Go!

Sign up for my mailing list and I'll send you a FREE sample from my course - Web Development with Go. The sample includes 19 screencasts and the first few chapters from the book.

You will also receive emails from me about Go coding techniques, upcoming courses (including FREE ones), and course discounts.

Avatar of Jon Calhoun
Written by
Jon Calhoun

Jon Calhoun is a full stack web developer who teaches about Go, web development, algorithms, and anything programming. If you haven't already, you should totally check out his Go courses.

Previously, Jon worked at several statups including co-founding EasyPost, a shipping API used by several fortune 500 companies. Prior to that Jon worked at Google, competed at world finals in programming competitions, and has been programming since he was a child.

Related articles

Spread the word

Did you find this page helpful? Let others know about it!

Sharing helps me continue to create both free and premium Go resources.

Want to discuss the article?

See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!

You can reach me via email or via twitter.

Recent Articles All Articles Mini-Series Progress Updates Tags About Me Go Courses

©2024 Jonathan Calhoun. All rights reserved.