DEV Community

Bruno Oliveira
Bruno Oliveira

Posted on

TDD and bug fixing - the duo you can't be without

Introduction

The benefits of working with a clean codebase and living up to quality standards when writing code are well-known: features are delivered faster, the code flow is easier to follow and clients and peers are both happier and more productive.

I'll approach this from a maintainability perspective and link it with how TDD (test-driven design) and bug fixing relate to maintainability and with each other.

Software maintainability - What is it and why does it matter?

Most software that professional developers work on, is already written and needs to be maintained and extended, so as a professional developer, you need to optimize your workflows for reading code and extend and improve large codebases.

Maintainability is an intrinsic characteristic of any piece of software that can be observed externally by using quantitative measures that, when looked at as a whole comprise the maintainability of a software system. It describes how easily a software team can make sense of the underlying business supporting the code and how easily they can add new functionality, correct bugs, cope with changing requirements and even onboard new developers on the project.

Some of the external characteristics that developers know about can comprise for example:

  • Coupling: how tightly intertwined two or more components in a system are. If when changing one, the changes ripple through other dependent components, it's a sign that they are tightly coupled. The more tightly coupled the components of a system are, the harder it is to maintain.

  • Number of parameters: as with the previous measurement, the number of parameters also influences the maintainability of a system.
    It's a good indicator that pieces of your system have too much knowledge when their unit interfacing (the number of parameters) is too long.

  • Function size: if single, individual code units are too large, it can potentially indicate that their scope is too broad or they're doing too much, which can hurt the maintainability of your codebase. Keep your units small and reap the benefits!

TDD and relationship with maintainable code

Test-driven development is a very useful technique in which you develop test-first: you write tests before writing the production code.
This allows you to write only the minimal code needed to ensure that the initial failing test passes. Like this, you ensure that you write exactly only the code you need to solve a specific problem and you ensure that all the code stays tested as you go, which is very valuable as it allows you to move forward faster and with more confidence.

However, this is also hard to do, as it forces us to think upfront in terms of the responsibilities each class will have in your overall system and ensuring that your methods are as small and isolated (meaning that they do one and only one single thing) as they need to be.

If you do this right, you'll have a very solid and strong foundation for basing your code on, in terms of adding new functionalities and fixing bugs as they arise while ensuring the whole system remains stable by running your unit test suite after you make your changes. This is invaluable when dealing with production-grade codebases as you want to ensure the systems remain stable after your work.

How to fix bugs using TDD

Now let's explore how to apply TDD to solve a bug.

For example sake, let's assume we have a simple calculator app, and that when we first designed it, by requirement, we were dealing only with the int data type when we implemented all the basic operations of addition, subtraction, multiplication and division.

Our bug tells us that a costumer system reports that 3/4 returns 0. Obviously, the data type we're using is incorrect, we need a floating-point data type. But we don't know this yet. What we know is that 3/4=0.75. This is our intended behavior, so we should write a test for this (in pseudo code style):

@test
void divisionReturnsDouble(){
    assertThat(calculator.divide(3,4), 0.75));
}

So in our unit test, we see that the test name declares the intent of what is being tested, and the test body is simply an assertion of the result we need to obtain.

So, we immediately see that the arguments we are using have the wrong data type.

We see that the Calculator class receives two parameters as integers, so, we change them to floats, run our single new test, we see that now it passes. We are done! We fixed the bug. Or did we?

Regressions and clean code

To our surprise, after running our entire test suite, we state that only our new test is passing!! All the others are failing! Why?
Upon close inspection we see that all the results for integer arithmetic operations are returning a decimal part as opposed to the integer value as before. This is actually correct since we changed the argument types to float. 3 is now 3.0, 6 is 6.0 and so on. We say that we introduced a regression. The correct and expected behavior of unrelated pieces of code changed after our change, so our supposed fix is defective. This is one of the main advantages of having an already ready to run test suite: you can ensure that your changes don't introduce unexpected side-effects in the code.

A smaller scope, means smaller problems

So we know that our code change introduced because of fixing division is causing a new bug. The key here is that we now know exactly what caused the bug, because we know exactly what we changed. Thanks to that we can revert it, re-run the tests and ensure that everything is working as expected. This is the main advantage of using TDD to fix bugs and as a vehicle for maintainability:

You work in small, atomic scopes of functionality and know exactly when something breaks. With tests, you're back in control of your code

The ideal scenario to fix this particular regression is then to ensure that the change we make in the code mimics the scope of the unit under test: it needs to be as small as possible. For our example, this could mean changing the data types only in the dedicated method for division, while leaving the rest of the code untouched. Granularity matters and TDD presents us with granularity up to the unit level, exactly what's desired and an indication of a good, well-thought codebase.

Conclusions

Hope you managed to learn about the benefits of TDD and its importance for the maintainability of a codebase. It's a very useful methodology to have in your toolbox, and, while it is hard to have a codebase ready to be maintained via TDD, the huge benefits are worth the eventual hurdles.
In a recap:

  • TDD presents you with very fine-grained, single purpose scopes that allow you to: reproduce bugs, start working on new features and refactor your code with confidence and speed.

  • Enforces an implicit standard dot quality: new developers will get up to speed faster and will increase their code quality to match what they first saw.

  • A fast feedback on your own changes, pulls, merges, etc. You control when and how you run it.

Hope you liked the article and if you have suggestions and/or comments, feel free to drop them in the comments!

Oldest comments (0)