Gustavo Santos

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.

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

Although I think that being able to test the 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 expending time in 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 these 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, 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 that apply it, think that it’s good. I’m one of these dudes that likes to do TDD in my regular job, I feel safer in the world doing TDD.

Personally, I do TDD not for the good of the project that I’m working on. Let’s be truth 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 a source code with high quality in 10 years”.

I feel safe having a test suite that I can run after each test 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 pencil to doodle diagrams and write it down all that 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 that 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, it 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 Jet Brains) 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 use properly basic stuff in the terminal. I think that it’s tedious being this kind of professional.

Start small

Do you know the times that 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 smell, 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 do it again.

There are two kinds of code

The production code is the code that runs (or get compiled into some code) that runs in production. It’s the code that produce 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 execises the production code against relevant aspects.

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

The test code also works as documentation.

Test suites with Playwright, for example, work like a living documentation. Or, at least, it 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 London school of testing the other is known as Classic school of testing.

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

You’ll see code that you might think that they are 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 happen that your tests have a lot of setup code that needs to run before and after each test case. It depends on how the test suite is organized.

I was the kind of guy that used to prefer the test trophy way to 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 necessary 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 good your team can communicate business logic, workflows, 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 adept at 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 live only 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 it’s fine.

TDD is the art of driving the writing of the 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 the 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 a code that already has tests exercising it.

Talking in refactoring…

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

I wouldn’t do it. I like to deliver my tasks in time and not to have regressions because one thing that I did wrong and there were no tests to catch my mistake.

If the project doesn’t have tests

Do 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 that produces code with fewer bugs, other developers will look into what you’re doing different 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’ve created a test suite for my own because the company “standards” required so much work to implement such 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 waited so long, sure that I must have been producing plenty of bugs since them, but not.

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