Test-Driven Development for the Uninitiated

Like many software developers fresh out of college, I felt I had a firm grasp of computer science but a less-than-strong knowledge of industry practices. One of my biggest weaknesses was test-driven development (TDD).

I’d heard of it. I knew it was important. I’d attended Computer Science club meetings on it. I’d even written a few tests for personal projects. But it wasn’t really part of how I thought about the development process.

Photo of Atomic employees Jeanette Head introducing Tyler Hoffman to test-driven development
TDD was a key skill I wanted to develop at Atomic object, and learning it was one of the largest shifts in my thinking about programming when I joined Atomic as a developer. This introduction to TDD is for people who find themselves where I was after college, especially if you think it is a good idea, but don’t yet feel like it will work for you. I will cover some of the basic ideas behind TDD and how to put them into practice.

An Introduction to TDD

TDD is, as the name suggests, the process of defining codebase tests that drive what code you write. In this model, tests come first. Once a test is written, you run it, and since the code to make the test pass hasn’t been implemented, your tests fail.

This motivates you to write code that will make the test pass–typically the simplest code that will result in a pass. Next, you write a test that covers another scenario, and you repeat the process.

Each time you update your codebase, you run your tests again, ensuring that you didn’t break previous functionality. The process gives you confidence that your codebase is always improving.

As I see it, there are three huge advantages of TTD.

  • It guides the code you write.
  • It ensures that refactoring worked.
  • It’s actually fun.

Why Use Test-Driven Development?

Writing tests seems like a lot of overhead. You have to learn the framework. You have to write a lot more code—maybe even more test code than real code. Sure, this approach ensures that the code you write works, but you could do that by running your program and verifying it works, right?

The fact is, manually checking all edge cases can be a huge pain. You’ll save yourself a ton of time by writing tests that can be run every time you make a change to your codebase.

When I was in data structures and algorithms classes, I spent an absurd amount of time manually checking inputs to see if my code worked. There are so many places where your code can go wrong. And even after manually checking, I couldn’t be sure things worked perfectly. I remember walking into my professor’s office to have him test the function of my binary tree with a feeling of dread, knowing he would check every edge case in greater detail than I had. If only I had written tests!

Guiding Your Development

As any programmer knows, you have to take things one small piece at a time. This is why we compulsively hit the “compile” button. We want to immediately check that our code is at least syntactically valid. And with every function we write, we check it with a variety of inputs, including all edge cases, to guarantee it works. You do that, right? Right?

Okay, so in reality, if you aren’t writing tests, you are probably checking for basic functionality, but beyond that, you’re crossing your fingers and hoping for the best.

Clearly, this won’t be sufficient when writing complex code.

When you write tests, you are explicitly stating what you want before you write code. At each step in the process, you check that you are making improvements without breaking previous functionality.

Ensuring Successful Refactoring

In my current project, I have had to make a number of changes that affect huge parts of the codebase. Sometimes I am able to make the change, run the tests, and find out that everything still works in less than a minute. In other cases, I see, to my dismay, that many tests fail after refactoring.

In one instance, I had more than 70 tests fail after making a change. This was painful, but it was fantastic to see where exactly things went wrong without having to hunt for cases where the code failed. It turned what could have been more than a whole day’s work into a fix that took about an hour.

Is it Really Fun?

Before I embraced TDD, I thought of a developer as akin to a great chef. I saw writing tests as the equivalent of food safety inspection. I wanted to be Gordon Ramsay, not the guy who comes by and scares all the restaurant workers. But I was wrong in this analogy.

TDD isn’t like that. For one thing, writing tests goes hand-in-hand with writing code. It’s more like a chef sampling a creation as he or she cooks.

I now get the nearly instant feedback that things work or break, so I can spend more time on the tasks at hand without worrying. It’s really rewarding to progressively see more and more green checkmarks coming back from tests as the code progresses.

Embrace TDD

As a final note, embracing TDD is easy. Once you get used to it, it just becomes part of how you write code.

Since I started writing tests before implementing functionality, I have had more direction in my development and more confidence in the quality of my code. If you aren’t writing tests already, I’d highly recommend that you start. Write a few unit tests on your next project, and see how you like it.