The right way to think about TDD

Written by on

I've been noticing quite a few posts in the developer community about ways to write better code. In particular, the Code Climate blog has been inspiring. A few days ago, Justin Searls inspired me further with his outstanding post about teaching TDD.

The reason Justin's post resonated with me so much was its relevancy. Just a few days earlier, I had exchanged comments with Bryan from Code Climate about the same thing.

Last week, Bruno (an engineer on our team) and I were talking about the community and the themes you extract from paying attention to the conversation within it. If you're a bystander, you'd probably notice the following rather easily:

If you're not following TDD, you're doing it wrong.

People preach TDD as gospel. It's overwhelming at times.

It's so overwhelming that you can wrongly interpret it. A beginner might think they have to do it all the time to learn programming. Nothing could be further from the truth. When you're first starting out, you're just trying to get things to work. Why should we encourage people to stumble through something that doesn't really help them do that?

The same thing is true for senior developers. If you don't practice TDD, you somehow have a letter tattooed on your chest that labels your code as buggy and awful.

What is our goal as software developers? I hope it's not to have the best test suite. You don't get a special medal for that, and it certainly doesn't imply you've designed and maintained a great code base. If I had a choice between a test suite with 100% coverage or a well-designed code base, I'd choose the latter every time. I can quickly write some high level tests to help me make future changes, but it would take me at least a month to undo years of bad design.

This certainly doesn't mean I don't see the value in testing. I'm simply trying to jump on the bandwagon with these guys - testing needs to be in a different part of the conversation, not at the forefront.

That being said, I've noticed a subset of the community changing the topic of the conversation. No longer is it TDD first. Instead, it's design first. It's writing the code you wish you had. If you design first, nice tests usually follow if you know what you're doing. If you're having trouble writing tests (using too many factories, slow running tests), it's an indication you have poor design.

TDD alone won't save you from violations of the single responsibility principle. It won't save you from poor design choices, and it certainly won't save you from costly refactors. With a little work, I can achieve 100% test coverage and end up with a code base that's messy and not maintainable.

It's bandwagon time. TDD isn't the only way. Don't follow TDD blindly.

Our goal should be writing amazing code. You should feel good about it. Things like the single responsibility principle, the open closed principle, and law of demeter are what you should be studying and applying to your work.

If you're writing code that follows these principles, it's mostly simple. Your classes are responsible for one thing. Your churn is low (your classes don't change often). You don't care much about the rest of the domain (your classes care about themselves). Most of your classes are easy to understand and you can re-use them with ease (they're loosely coupled).

Simply following TDD doesn't give you the beauty of great design. It puts a band-aid on the issue.


Here's how we've been operating as a team.

We don't always write tests first because you don't know what you don't know. The goal is to get nicely written, working code and NOT perfect tests and a badge that says you followed TDD.

We do think about testing as we're designing. If we're creating a new class, a spec file is sure to follow.

We focus on great naming and try to use namespacing. Being able to come up with a good name is a sign you've broken down the problem enough. For example, if we wanted to create a class to notify us about comments on this blog, we'd use "Blog::CommentNotifiers::Email" which would notify us by email. It's simple and it will end up doing one thing: sending an email when a new comment is posted on the blog. Using a callback on the "Comment" model would be a poor design decision and require you to opt-out of the behavior whenever you don't need it.

We focus on unit tests. If we have to stub out all kinds of classes and methods (more than 10 or so), we treat it as a code smell and think about refactoring.

We've learned a lot in the past year about writing better code. When we have to write new code, we write code we wish we had. We don't look for the quickest way to fix the problem (although there are always exceptions). We focus on very clear class names and private methods that read in plain-English. We try to avoid obscure method names. We focus on low-level implementation details last - once we have the design complete.

I highly, highly recommended the following resources to assist you in writing better code: