Getting started with TDD is easier than you thought, but only if you know what you're doing

Let's talk about how to begin with TDD.

Gustavo Santos Sep 24, 2023

Before we start this conversation, be warned that it might not be applicable to all contexts.

Although I think that being able to test code is essential for any kind of project, there are cases where we just don’t give a fuck about tests. There are projects where we are just playing with new stuff to see if it is worth spending time on it, such as learning a new programming language, a new web framework, a new API to write embedded code, and so on.

This text is not about those cases; this text is about production-grade applications that must evolve and keep working iteration after iteration.


While browsing the web searching for TDD because you heard someone saying that “TDD is good for software development,” or “just do TDD because I’m saying so, and I’m a senior developer,” you might get trapped in a hole where someone is saying that you need to do something that you don’t even know what it is.

It’s normal; TDD is not a monster that you need to deal with. TDD is just a technique that the folks who apply it think is good. I’m one of those dudes who likes to do TDD in my regular job; I feel safer in the world doing TDD.

Personally, I do TDD not just for the good of the project that I’m working on. Let’s be honest with ourselves: you work on a project for some time and then you move on. It’s very rare to find people thinking in the very, very long term—something like “we need to practice TDD now to have source code with high quality in 10 years.”

I feel safe having a test suite that I can run after each change to know if I broke something. I also feel comfortable writing the test first as a guide to know what I need to do and how far I can go without producing a new commit. It’s easier to break a huge task into small, simpler tasks that I can deal with one by one, using the tests as a guide.

Sure, some folks just need paper and a pencil to doodle diagrams and write down everything they have to do in a to-do list. It’s fine; it works for them.

Not for me.

Perhaps it might not work for you as well.

Thinking about these folks, I came up with a small guide that I think might help you all in the wild environment of software development.

The very first step

Get comfortable with your tools.

I like to think that we have a tool belt where every new thing that we learn transforms into a tool that we hold in our tool belt. Sometimes we store the tool in the wrong way. Sometimes it takes time to get used to something.

IDEs are the best example that I can come up with. IDEs are professional tools and even so there are professional folks out there (me included) that prefer working with something different—Vim, for example. Others prefer Emacs, others prefer Sublime Text.

But IDEs (and I’m looking at you JetBrains) deliver so many tools that it’s hard to know how to work with all of them in the first months. It requires learning, and learning the tools that you are using is the key.

If you do Java, learn how Maven works, how Gradle works, how JUnit works. Do not limit yourself to clicking a button in your IDE; learn how to interact with these tools on the command line—all of them have a CLI. In fact, many times, the IDE is doing the dirty job of using the command line to present you that nice button.

For instance, there are a plethora of Neovim plugins that integrate various test runners, linters, and formatters right into the editor. What do they do internally? They know how to interact with the CLI of your project tools; they know how to format a file using Prettier, they know how to lint a file using ESLint, they know how to run the current test file with Jest, Vitest, Playwright, and so on.

Some folks at the beginning of their career fear the terminal. I know developers that even after 10 years working with software, they don’t know how to properly use basic tools in the terminal. I think it’s tedious being this kind of professional.

Start small

Do you know those times when you have a Jira ticket to work on, and you know exactly how to solve the problem?

This is the situation where you can write your first test before writing the production code.

Write the first test; it probably will be small. You likely will write tests with code smells; it’s fine. No one is born knowing how to write great tests.

The first step to sharpening your knife is to have one.

Write the test, watch it fail. Then write the code and watch it pass. You’ll see that this is a life changer. You’ll like the feeling so much that you’ll do it again.

There are two kinds of code

The production code is the code that runs (or gets compiled into code that runs) in production. It’s the code that produces the product that end users interact with.

The test code is the code that is executed by the test runner. The idea is that the test code exercises the production code against relevant aspects.

Both types of code are important. They play different roles inside the software, but with equal importance. If you expect to read simple production code, you might expect the same from test code. Just beware that some kinds of optimizations—mostly related to Clean Code stuff or other fancy names that we give to certain techniques—may not fit well for test code.

The test code also works as documentation.

Test suites with Playwright, for example, work like living documentation. Or, at least, they should.

Beware that there are many ways to write tests

If you deep dive enough into the TDD world, you’ll discover that there are basically two ways of writing tests: one using mocks and the other that avoids mocks. The first is known as the London school of testing; the other is known as the Classic school of testing.

I used to avoid mocks; now I use them when I need to.

You’ll see code that you might think is not the right way to write tests; perhaps the code that you saw relies on mocking, perhaps not. One is not better than the other.

If you avoid mocks, it could happen that your tests have a lot of setup code that needs to run before and after each test case. If you use mocks, it could also happen that your tests have a lot of setup code. It depends on how the test suite is organized.

I was the kind of guy that used to prefer the test trophy way of writing a test suite. Most of my old tests were written as integration tests. They end up being slow, having too much context inside each test file that makes the tests harder to understand and also difficult to maintain.

Code with tests isn’t necessarily better than code without tests

Your code can even be wrong with tests and right without tests. You just need to write the test to validate the wrong thing.

TDD is a complementary tool that you can use with the most important tool of all time: communication.

The tests will reflect how well your team communicates business logic, workflows, and meaningful names using a ubiquitous language, and so on.

If the team that you are working with cannot communicate well enough to build a product, the code will be a mess and the tests, if there are any, will also be a mess.

I’m an advocate of trying. Try fixing the communication, for example; suggest enforcing user stories to at least have a list of acceptance criteria—a list of stuff that is expected to happen after the implementation.

If it doesn’t work, get a new job. You only live once.

TDD is a tool to guide you; you need to at least have a map. Without the map, the guide is useless. You don’t know where you are going without a map.

Just writing tests is not TDD

And that’s fine.

TDD is the art of driving the writing of software guided by tests. To do that, you must write the test first.

You don’t need to always use TDD to develop something; it’s fine.

I use TDD only on production applications. Sometimes I use multiple layers of TDD: first TDDing at a high level of abstraction with browser-based tests, then proceeding with unit tests after having at least one test guiding the overall implementation. Sometimes I just refactor code that already has tests exercising it.

Speaking of refactoring…

You can do it without tests; it’s fine.

I wouldn’t do it. I like to deliver my tasks on time and not have regressions because of something I did wrong and had no tests to catch.

If the project doesn’t have tests

Take the first step and set up an acceptance test suite. If you work on web-based projects that have a front-end and a back-end, be sure that your feature can be exercised at the user level.

Frameworks such as Playwright, Puppeteer, Cypress, and so on will help you.

Write the first test for the most critical path that you are responsible for developing. Then grow the test suite.

In a few iterations, you’ll become the one who produces code with fewer bugs. Other developers will look at what you’re doing differently from them. And then you will shine like a star, saying that you are now writing tests and they are helping you a lot.

In the past, I created a test suite for myself because the company “standards” required so much work to implement such a test suite inside the regular repositories that I created my own automation project. It grew for many months until the company finally saw the value of that test suite.

Sometimes it takes time. If I had waited so long, I surely would have been producing plenty of bugs since then, but I wasn’t.

I must note that writing tests doesn’t eliminate the presence of bugs. It’s the combination of testing first, communication with the team, and fast feedback that prevents bugs. Even so, it’s not a guarantee.