Advantages of Test-Driven Development: Why you should always test your code

Test-Driven Development (TDD) was widely spread after the release of Extreme Programming Explained by Kent Beck. TDD consists of writing tests before actually implementing features in a project and may be very helpful in many cases.

Gustavo Motta

Gustavo Motta

July 06, 2022 | leitura de 8 minutos

dev

Test-Driven Development is a well-adopted technique that has been used in many cases. But before we talk about writing tests before features, I'm going to give a brief introduction to the importance of testing and why we should always test our programs.

Why Testing?

You may have come into a situation where a feature is quite complicated and complex, and every time you changed your code a little bit it got even harder to deal with.

Or maybe another feature that was easy to change at first glance, but after you finished developing it, it broke something else, so you had to go back to understand what happened so you could continue the development process. Building software is not always about following a straight path that is defined from the beginning. A lot of the time it is just not.

Many times you will have to make corrections that can vary from small to huge during development, and they may become a lot harder to deal with if you don't have the coverage or the confidence your tests could be giving you.

That is one of the most important and valuable advantages provided by tests, confidence. Automated tests are responsible for making the development easier in the sense that if they are properly set up, you may be sure your program is working as it should without having to test features manually.

If you add a new feature, your tests will break and you will know it before sending it to production. If that reason is not enough for you I can give you a few more:

  • Time: as bugs could be detected in minutes, not hours (or even days), you won't be spending so much time trying to find them.

  • Clean code: tests can be very hard to work with when you are dealing with coupled functionalities. By writing tests you will have a better idea of how to make them easier to work with, and by doing so, it will also make it easier to write cleaner, more cohesive code.

  • Projects live longer: having tests makes your project easier to change. If you have no tests, chances are the cost of change will become certainly unbearable at some point (see the graph below). When that cost gets too high, the project won't be able to evolve anymore.

cost of chage vs. dev time.png

Source: freeCodeCamp

With all that said, hopefully, the importance of writing tests is clarified. Tests should not be written just because someone said you should write them, they should work as a powerful tool for you to use in your projects.

Test-Driven Development (TDD)

As contradictory as it may seem, programming and testing together have many times been proven to be faster than just programming.

The graph below displays results obtained on an experiment that consisted of implementing integers to roman numerals converter multiple times with and without TDD. Note that even the slowest TDD implementation was faster than the fastest No TDD implementation.

Clean Architecture: A Craftsman's Guide to Software Structure and Design.png

Source: Clean Architecture: A Craftsman's Guide to Software Structure and Design

If you think about it, every time you implement a new feature without its respective tests, you'll have to debug it manually, and after that, you would go on and write the tests, which would take even more time.

If you are testing your software, it is still a good approach, but being able to debug a feature automatically while you're still implementing it really can make your day more productive, as you would spend a lot less time finding bugs. But how can we approach writing tests first?

The Red-Green-Refactor Cycle

Test-Driven Development follows a process of three phases:

  • Red: Write a failing test of the feature you're implementing.

  • Green: Implement the feature so it can pass the test you've just created.

  • Refactor: Improve your code without adding new features, making sure your tests still pass.

A Polyglot Guide to Writing Uncluttered Code.png

Source: Learning Test-Driven Development: A Polyglot Guide to Writing Uncluttered Code

In the real world before writing a test you should have an idea of how your system should behave and which requirements it should meet.

Keep in mind that by following TDD, many times you will get a better understanding of how your feature should be implemented while writing the tests, so the RGR cycle should happen many times during the implementation of a feature.

The moment you realize that there are no more tests that could be created to fit the feature's needs it means you're done, that is, you've met all the requirements the feature had. Also, the feature is already tested.

Structure of a Test

Okay, now we know how the TDD cycle should work. But you could be wondering: how do I write a single test? What is considered good practice for this? Is there a more correct approach?

There are many approaches to writing tests. There are many different types of tests as well. In this post I'll be focusing on unit tests, using the AAA approach, which stands for Arrange - Act - Assert. It is quite a simple approach, as there are just three simple steps that should be followed:

  • Arrange: Here you set up an environment for what should be tested.

  • Act: Here you execute the feature so it can be evaluated in the final step.

  • Assert: Here you will check if the expected results were met.

Now that we have an idea of how a test should look and how TDD works, we can finally go and implement an example feature. And of course, as you would expect, tests first!

Unit test - Developing an example feature

Unit tests are meant to test individual units of an application, which many times are functions running in a single context. It really should be clearer to see with an example, so for that, I've chosen one of the most popular programming languages today: JavaScript.

Many good libraries could be used for testing that I could bring here, but as this is not the purpose of this post, I'll just use the Node.js assert function.

We'll be implementing a simple function that receives an array of integers and returns the highest one. With that said, here is our test:

const assert = require("assert");

/* ARRANGE */
const numbers = [1, 9, 20, 2, -3, -100];

/* ACT */
const result = getHighestNumber(numbers);

/* ASSERT */
const expectedResult = 20;
assert.strictEqual(result, expectedResult);

Just in case you might be wondering, if you were using a testing library like jest, for example, your test would probably look more like this (in a .test.js or .spec.js file):

 
test("getHighestNumber returns highest number from an array of numbers", () => {
 const numbers = [1, 9, 20, 2, -3, -100];
 
 const result = getHighestNumber(numbers);
 
 const expectedResult = 20;
 expect(result).toEqual(expectedResult);
});
 

Note that the AAA pattern is still being followed here, just like we did before by using only the assert module.

We've reached the Red phase of the RGR cycle. Now if we try to run our tests we will be getting an error, as the function getHighestNumber has not been implemented yet. So let's go and implement it just so our test passes:

const getHighestNumber = (numbers) => {
let highestNumber = Number.NEGATIVE_INFINITY;

for (const number of numbers) {
  if (number > highestNumber) {
    highestNumber = number;
  }
}

return highestNumber;
};

Great! Now we have a working tested function for our new feature! If we run our test, the result should be green. But still, you could say our function could be more straightforward than this, by using callbacks. For that we refactor it:

 
const getHighestNumber = (numbers) => {
 return numbers.reduce((number, highestNumber) => {
   return number > highestNumber ? number : highestNumber;
 }, Number.NEGATIVE_INFINITY);
};
 

And after a small refactoring of our function, we still get our tests passed. That means we've completed an RGR cycle and we are done creating this feature!

Although this was quite a simple example, I hope it helped to clarify how the TDD cycle works and how a simple unit test could be implemented to produce a feature.

Well, it looks like we've reached the end of this post. If you have gotten this far, I hope I was able to show how great tests and TDD are proven to be, and how you could use them. I also hope you've enjoyed the content and that it helps you in your projects.

Happy coding!

References

  • Extreme Programming Explained: Embrace Change, Kent Beck, 2004.
  • Learning Test-Driven Development: A Polyglot Guide to Writing Uncluttered Code, Saleem Siddiqui, 2021.
  • freeCodeCamp: Test-driven development might seem like twice the work — but you should do it anyway, Navdeep Singh, 2017.
  • Clean Architecture: A Craftsman's Guide to Software Structure and Design, Robert C. Martin, 2018.
Gustavo Motta
Gustavo Motta

Software Engineer. Interessado em tecnologia em geral. Gosto de problemas que podem ter mais de uma solução e visões diferentes de mundo.

LinkedInInstagramGithubMedium