Does TDD slow you down?

When you first start out with TDD, development will be much harder and much slower. It will practically grind to a halt. This is because you are learning. I’m not as interested in this part of the discussion. Any time you are learning something new, you will go slower. The more interesting question is, is it worth learning? Does it still slow you down once you become competent?

The truth is, you may never be as fast with TDD as you were without it. That’s a sign that you were going too fast. You weren’t finishing your work. You were writing code to get that specific feature working, and then moving on. You didn’t have to worry if the code you wrote was tightly coupled or had a poor interface, because you only had to call it once and that work is done. You definitely didn’t do much refactoring, because there was no safety net in place to alleviate the fear.

That pace is super fast and very addicting. But it is not sustainable. You can get things built quickly, but eventually maintenance becomes a nightmare, and your progress grinds to a halt. That’s because the same qualities that make code hard to test make it hard to change. Building code is easy, maintaining (i.e. changing) code is the hard part. TDD forces you to start feeling that pain early, so the cost gets spread out over the life of the codebase, instead of pushed back and back until you’re forced to deal with it (technical debt!).

So it’s about trade-offs. If you are working on a quick prototype, don’t write tests. It will slow you down! It’s ok to admit that. But it’s fine, because tests for a prototype won’t provide value. But if you are building something to last, write tests. It may slow down your initial velocity, but it will even out over the long-term life of the project.

Have you been trying to do TDD and it still feels like it’s slowing you down too much? Does it seem like your tests are doing more harm than good? Are you still waiting to see all these supposed “benefits” of TDD? I’m working on some materials to help you level up your TDD skills so you can start loving your tests instead of hating them. And I started a newsletter so we can have a conversation about the pain you’re feeling, and I can let you know as soon my TDD materials become available.

How to test your tests

One of the benefits of writing your tests first is that you will always see the test fail. This is important, because you can’t trust a test you haven’t seen fail. Think about a line of code like this: assert a = 3

Of course, you meant to write a == 3, but you may not realize that if it’s in a test that you wrote to verify already-working code. It would pass, and you’d assume it passed because the code it’s testing really did set a to 3. But if you wrote and ran the test first (or commented out the working code to see a test failure), you’d notice that the test was passing when it shouldn’t, and fix the bug.

Watching a test fail is one way to test your tests.

But don’t just see a red/failing test and run off to make it pass. Pay attention to the failure message. You could have a different bug in your test that’s causing the wrong failure. Maybe due to a syntax or logic error. So if you don’t get the failure you expect, that’s another sign that your test may have a bug.

Now when you see a failure you expect, only write just enough code to fix that specific failure. Is your “makes a equal to 3″ test failing because the module is missing? Don’t implement the entire module, just create it. Then watch it fail because the function is missing from the module. Now don’t implement the entire function, just declare it. And so on. Keep fixing only the immediate failure until you hit green. Does the implementation feel complete? If not, you need another test.

It may feel silly at first, but if you train yourself to always take these micro steps, you can be sure that every line of your code is actually being tested. If you take large steps, the chances increase that untested – or even superfluous – code sneaks into your system. Fixing only the current failure tests your tests for completeness.

So while you’re in the “red” step of “red, green, refactor”, remember to keep an eye out that you’re red for the right reason, and don’t try to jump straight to green, just fix whatever is making you red right now. Eventually you’ll get there, and you’ll feel super confident in your code.

Do I have to write the test first?

To many, writing the test first is a requirement of TDD. It’s how I prefer to do it, but I don’t believe it’s a requirement, especially when starting out.

But that doesn’t mean I’m suggesting you go ahead and code away willy nilly and then write all the tests when you’re done. You still need a tight feedback loop. So how do you get that if you aren’t writing the tests first?

Using small steps: write one slice of code. Does it work? Good, now comment it out! Then write a test that will only pass with the code you just wrote.

Now run your test and watch it fail. This is an important step. If you haven’t seen a test fail, you can’t trust that you’re actually testing what you think you’re testing.

Now uncomment your code. Does the test pass? Good. Now you can refactor. Does the test still pass? Good! Now commit and repeat. I sometimes call this comment-driven development. I’m sure I’m the first person to think of it. I’m very clever.

If you stick to this style, eventually you will start to anticipate how to design the code you’re writing so you can easily test it. Then you may decide it’s easier to just go ahead and write the test first. Welcome to the club.

TDD, Micro Steps, and Backing Up

TDD is a way to think through your requirements incrementally by writing a test for one small piece, writing just enough code to get that test to pass, refactoring, and then moving on to the next small piece.

As you’re growing your code in this way, you should be zoomed way in. Taking tiny, micro steps. The time from failing to passing test should be measured in seconds, not minutes. It’s a feedback loop and it should be tight. You don’t want to waste time poking around in the dark.

This puts excellent design pressure on your system. The only way to keep that tight feedback loop moving is by writing code that is loosely coupled with a single responsibility. Otherwise it will be too hard to test.

This is why TDD is more about design than it is about verifying working code. But that doesn’t mean you can ignore design completely and let your tests lead you blindly somewhere without thinking. You will need to zoom back out every once in a while. You will need to put your knowledge of good design principles into practice and think critically about your code beyond what’s only easy to test.

I’ve been in many positions where my tests painted me into a corner. Or my feedback loop starts slowing down as complexity starts spiking, or the easiest way to test something would result in an obvious code smell. When that happens, I back up.

The ability to back up is another reason to keep the feedback loop tight. You should always be able to easily jump back any number of steps to working code and try a new path.

Yes, you still have to choose your path when you TDD. It’s called test-driven development, but you’re still the driver, not the tests. They are a tool you use to drive out some desired behavior, and there’s usually going to be multiple ways to write tests to get there. Use your design sense to make the best choice. If you don’t like where you ended up, back up. And keep your feedback loop short so backing up is no big deal.