How TDD Can Prevent Over-Engineering
Tags: tdd, programming, javascript, agile
Published: 2023-05-13 05:44:57

---
title: How TDD Can Prevent Over-Engineering
published: true
tags: tdd,programming,javascript,agile
canonical_url: https://fagnerbrack.com/how-tdd-can-prevent-over-engineering-1265a02f8863
---

#### A [discipline](https://hackernoon.com/how-to-fix-the-software-industry-c2b627ec3d9d) that can force you to write only the code you need

![](https://cdn-images-1.medium.com/max/1024/1*hrqVZSit6lBy98F0ySpHxA.jpeg)<figcaption>The picture of a bald man with a short beard. He’s using a computer on a table and smiling. It's the same bald man of the picture from the <a href="https://dev.to/fagnerbrack_2/the-missing-practical-step-by-step-tdd-ih9-temp-slug-5508164">first post</a>, only now there's no dark shadow surrounding him.</figcaption>

Refactoring is essential. It allows you to modify the structure of the code with the least amount of effort. In this last chapter for the story of [“Jack, The Moneylender,”](https://dev.to/fagnerbrack_2/the-missing-practical-step-by-step-tdd-ih9-temp-slug-5508164) you'll see how TDD as a [discipline](https://hackernoon.com/how-to-fix-the-software-industry-c2b627ec3d9d) can prevent you from falling into the trap of [over-engineering](https://hackernoon.com/how-to-accept-over-engineering-for-what-it-really-is-6fca9a919263).

In the [first post](https://dev.to/fagnerbrack_2/the-missing-practical-step-by-step-tdd-ih9-temp-slug-5508164), I started with the story of Jack, a moneylender searching for better ways to calculate the interest of his loans.

It goes like this:

> Hi, I’m Jack. My business is to give loans. I can make a small loan to you, but if you do, then I'll charge interest for each dollar above a certain threshold.

> Can you build something that can do that calculation for me?

> $0 to $2000 = No Interest

> $2001 to $5000 = 9 cents per dollar

> $5001 to $10000 = 14 cents per dollar

> $10001+ = 21 cents per dollar

After [the first post](https://medium.com/p/a7140ca4b71), Jack ended up with a product that can calculate $0.09 of interest for each dollar above $2000 up to infinity. That was the first boundary of the problem. After [the second post](https://dev.to/fagnerbrack/this-is-the-one-thing-nobody-told-you-about-tdd-102-temp-slug-1786179), Jack ended up with a product that can calculate $0.14 of interest for each dollar above $5000.

Now it’s time to develop the code for the next boundary and refactor the code to remove the duplication. This time, the code needs to calculate $0.21 of interest for each dollar above $10000.

Like before, there’s a [repository](https://github.com/FagnerMartinsBrack/jack-the-moneylender/commits/v3) where you can see one commit per test run. That repository shows the Red/Green/Refactoring steps in a separate commit.

- The commit has a 🔴 when it represents the **red**  step.
- The commit has a green ✅ when it represents the **green**  step.
- The commit has a 🔨 when it represents the **refactoring** step.

Let’s begin.

> Jack is happy with the outcome so far. Nothing could be better than this… could it?

In the [last post](https://dev.to/fagnerbrack/this-is-the-one-thing-nobody-told-you-about-tdd-102-temp-slug-1786179), you learned how to write the code to calculate $0.14 of interest for loan amounts above $5000. You can follow the same pattern to create the code for the interest of loan amounts above $10000.

The first step is to create a passing test for one dollar below the next range. The reason to create a test for that is you can recap how the existing algorithm works:

{% gist https://gist.github.com/FagnerMartinsBrack/475ff9226f9392d4eded1b101855f592 %}

Next, you can create a test for the first dollar inside the next range.

{% gist https://gist.github.com/FagnerMartinsBrack/e5a49726edcd7aab41a75692e1cb6ff1 %}

You expect the existing algorithm of $0.14 for every dollar to be still in effect. Therefore, the test fails with a meaningful message.

![](https://cdn-images-1.medium.com/max/1024/1*dAquOqW_yA7g3glSpUEkOw.png)<figcaption>The screenshot for the test failure for $10001 loan amount. It expects $970.21. Instead, it gets $970.14.</figcaption>

Now follow [the same steps](https://github.com/FagnerMartinsBrack/jack-the-moneylender/compare/a9c2ee4085d8e82a9256909239b2585f788d301d...ea640a56f0df1141f6103edd63d90019462a804f) as the [previous post](https://dev.to/fagnerbrack/this-is-the-one-thing-nobody-told-you-about-tdd-102-temp-slug-1786179). Isolate the range for a loan amount greater than $5000 in the previous condition and develop the interest for loan amounts above $10000 in a new condition:

{% gist https://gist.github.com/FagnerMartinsBrack/6a814bb91f3a5663c485ecf290abe58b %}

The result is a new condition with a lot of duplicated code. Also, the test fails because you're missing the rules to calculate interest of $0.09 above $2000 and $0.14 above $5000:

![](https://cdn-images-1.medium.com/max/1024/1*XvSCJ5ayp-5cCYNjZGAFKA.png)<figcaption>The screenshot for the test failure once you isolate the previous ranges and create the code for the calculation of loan amounts above $10000. It expects $970.21. Instead, it gets $0.07.</figcaption>

Once you copy the calculation for loan amounts above $5000, the error message changes:

{% gist https://gist.github.com/FagnerMartinsBrack/ba367bea4b4d71a00d282e0e71cf32b2 %}

![](https://cdn-images-1.medium.com/max/1024/1*P8MYcGLnRYEIU2WvU8MILQ.png)<figcaption>The screenshot for the test failure once you copy the calculation for loan amounts above $5000. It expects $970.21. Instead, it gets $250.12.</figcaption>

Once you copy the calculation for loan amounts above $2000, the test passes:

{% gist https://gist.github.com/FagnerMartinsBrack/04583d2bb2bd41c0463beaed8e54e5d3 %}

Here's how the code looks like once you remove all Magic Numbers:

{% gist https://gist.github.com/FagnerMartinsBrack/620790572bdd7875d6fb4284eff95411 %}

Right now that seems like a mess. The code has a lot of duplication and hard-coded values everywhere. However, this is the kind of messy code that was **driven** by tests. Therefore, it contains many patterns that can lead to insightful discoveries.

To uncover those patterns, you need to [refactor](https://fagnerbrack.com/how-to-refactor-a-public-interface-317ed18d38a3). You need to apply small changes to the code without altering its behavior. The way you see that you're not altering the behavior of the program is when you apply the changes for a module/class/function — like saving, — and the behavior of the program doesn't change.

That’s the reason why it’s so critical to start with [Tests-First](https://fagnerbrack.com/code-review-and-test-driven-development-4c19b69b5761). If you don’t write Tests-First, it's harder to ensure that you're testing the right things and that the behavior of the system won’t change when you refactor. In the same way, without practicing Test- **Driven** , it's hard to understand if you’re increasing or decreasing the level of transformation according to the [Transformation Priority Premise](http://blog.cleancoder.com/uncle-bob/2013/05/27/TheTransformationPriorityPremise.html).

> You know you're refactoring when you change the code and the following remains true: the [level of transformation](http://blog.cleancoder.com/uncle-bob/2013/05/27/TheTransformationPriorityPremise.html) of the code doesn't decrease, the tests stay green, and future tests which follow the same pattern would also stay green.

At this point, the code has duplication for each conditional. An effective way to [remove that duplication](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) is to create a function with arguments for the values that change.

However, it's hard to know how that new function should look like beforehand. If you want to increase the chances for the tests to stay green all the time and keep the changes small, you can start with [pure functions](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-pure-function-d1c076bec976) that are **very specific** to their purpose. You can modify them to be more generic later.

That said, create a new function for the calculation of interest rates when the loan amount is higher than $2000. It's a good idea to keep the function closer to the code you're extracting so that you can see in which position the arguments should be.

{% gist https://gist.github.com/FagnerMartinsBrack/9fd0dac8f39101d85fb74a4f92938a38 %}

After that, it's a good idea to lift the function that calculates the interest to a scope outside the primary function "interest to pay for." Although this violates the [Strictness Principle](https://fagnerbrack.com/the-strictness-principle-9997e483cafb), which states you should keep variables only in the scope that's using them, it also allows you to verify that the function doesn't access any external variables, the "side-effects." If the function you create has access to external variables, it's hard to change it. If the tests don't break after moving it, that means the function has no side-effects.

After you make sure that the tests pass, lift the function outside the scope and replace the logic everywhere else.

{% gist https://gist.github.com/FagnerMartinsBrack/b469829e55073e185e632426f34a3121 %}

> When you refactor code to a new function, verify if it doesn’t have side-effects.

If you do the same thing for each one of the other calculations, you'll end up with a code that looks like this:

{% gist https://gist.github.com/FagnerMartinsBrack/9dd9ebde85c9b8f87ac9b2f294b5a965 %}

You still have duplication, but it looks better than before. There’s one function to handle $2000, one function to handle $5000 and another function to handle $10000.

{% gist https://gist.github.com/FagnerMartinsBrack/437af3041ba54b6e183f0cdf6511e6da %}

When you refactor, and there's duplication, it's essential to keep the functions as similar as you can to each other. As humans, we are pattern recognition creatures. If you have code that looks the same, it's much easier to understand the problem and discover meaningful patterns.

Notice that the first function to calculate loan amounts above $2000 is missing one argument to have the same number of arguments as the other functions. You can fix that.

{% gist https://gist.github.com/FagnerMartinsBrack/81d18d345383cd38606d832ec55bb5a3 %}

Also, the internal variables and arguments for all the functions have different names. Let's make them the same.

{% gist https://gist.github.com/FagnerMartinsBrack/f2ec7b821a456e0121ded18ee3f749a5 %}

You can see now that all the functions accept the same things:

- The loan amount.
- The amount that represents the "end of the range."
- The amount that represents the "interest per dollar."
- The amount that represents the "previous interest per dollar."

The “loan amount” is a fixed value. It’s the input that only changes in the context of the primary function “interest to pay for.” The value for the "loan amount" won't change throughout the execution of each calculation.

The other arguments are different:

1. The code calls the functions with a different value for the arguments “end of the range,” “interest per dollar” and “previous interest per dollar” depending on which calculation is running.
2. The functions to calculate the interest for each range uses [Connascence of Position](http://connascence.io/position.html) for its arguments, instead of [Connascence of Name](http://connascence.io/name.html). That's a [Bad Code Smell](https://fagnerbrack.com/code-smell-92ebb99a62d0).

To fix the Bad Code Smell and discover why the code calls the functions with the different arguments, you can apply the [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) approach. Create one Object Literal representing the arguments that change, then [reuse](https://fagnerbrack.com/how-to-solve-the-challenge-of-reusability-ac4edce069c9) them. You can start with the range of calculations for loan amounts above $2000:

{% gist https://gist.github.com/FagnerMartinsBrack/37b3186b18f61c84ec3d71f11995a51d %}

Then, as a second step, uplift the Object Literal outside the function. Given this is an [interface breaking change](https://fagnerbrack.com/how-to-refactor-a-public-interface-317ed18d38a3), you need to update all the external function calls inside the other conditionals for the tests to remain green.

{% gist https://gist.github.com/FagnerMartinsBrack/bfc4cbf4cb9822ef2c9a386030911271 %}

If you do the same thing for the other ranges and delete the duplication completely you'll end up with a piece of code that exposes a new pattern:

{% gist https://gist.github.com/FagnerMartinsBrack/e626c294f34d053b6db5e5bcd0ff29df %}

You can see the [commits](https://github.com/FagnerMartinsBrack/jack-the-moneylender/compare/9c1093bdc3e2088d756d034eb73d9b7e7520cedc...d886524c857c48a572d37aa3f98044a3efc00374) which lead to that result.

Now that you refactored the code, you can see that there's only one return, which is the "interest amount," but the code duplicates it inside every condition.

Let's remove that duplication:

{% gist https://gist.github.com/FagnerMartinsBrack/6b34237b25486e197d10a8dfe8aa87aa %}

Now look carefully at the code:

{% gist https://gist.github.com/FagnerMartinsBrack/6c975dc1f2a3d0b199e7a4cb0d7746ab %}

You can see the condition for a loan amount greater than $5000 repeats the calculation for a loan amount greater than $2000. The reason it repeats is that the first condition only runs if the loan amount is less than $5001.

You can dump the right-hand side conditional of the first condition. If you do, you fix the duplication:

{% gist https://gist.github.com/FagnerMartinsBrack/9a81c2cff728521ccebd23048377c094 %}

You can do the same thing for the rest of the code:

{% gist https://gist.github.com/FagnerMartinsBrack/027e89fe284d40bd1074f1ebef217a5a %}

Here's the result:

{% gist https://gist.github.com/FagnerMartinsBrack/5f1bbe0803341f8c3b5769aae51f5d06 %}

The code above clearly shows how the algorithm calculates the interest if a “loan amount” is greater than $2000, $5000, or $10000.

Now here's the **mind-blowing** moment:

When you refactor the code to make each component similar to each other and remove [Bad Code Smells](https://fagnerbrack.com/code-smell-92ebb99a62d0), not just the code becomes painless to maintain, but you also understand better the patterns of the [problem you are trying to solve](https://dev.to/treyhuffine/the-problem-you-solve-is-more-important-than-the-code-you-write-2b0g-temp-slug-2836748). This way, you know you are generalizing in the [right direction](https://hackernoon.com/the-journey-for-the-right-question-c3f5b9e90035) without speculation or [over-engineering](https://hackernoon.com/how-to-accept-over-engineering-for-what-it-really-is-6fca9a919263).

> In Test-Driven Development, you only write the code you need. Nothing else.

Another interesting thing you can see is that there's a decoupling between the ranges and the code that runs the calculation on them. You can extract the ranges into a JSON configuration file. If you do that, Jack can modify the behavior of the code by modifying the config file. He doesn't need to pay a developer every time he wants to modify behavior that follows the same pattern.

If you don't want to extract the ranges into a configuration file, you can still refactor the code to emphasize the decoupling. When you emphasize decoupling, you also help to increase the legibility of the code, regardless if you move the data to a configuration file or not.

> Test-Driven Development allows you to understand the problem and create more value.

{% gist https://gist.github.com/FagnerMartinsBrack/f5c85286c7b8bb50e757ab51d475b67b %}

{% gist https://gist.github.com/FagnerMartinsBrack/7a685f6166adc7c8c6e552cc4c2d6d11 %}

That's it! Here's the final code:

{% gist https://gist.github.com/FagnerMartinsBrack/95b1b2dce6a72e19b99425c5e64fba62 %}

If you've been reading this since the [first post](https://dev.to/fagnerbrack_2/the-missing-practical-step-by-step-tdd-ih9-temp-slug-5508164), now you should understand every detail of "Jack, The Moneylender" problem. That means you can continue refactoring the code as much as you want and be confident you never introduce bugs unintentionally.

Test-Driven Development and refactoring may sound like a tedious process. However, as with any skill, you get better over time. With practice, your [velocity](https://fagnerbrack.com/it-doesnt-matter-how-fast-you-write-code-2e09b42bfff0) increases. Next time you get a similar problem, you may discover the patterns earlier and finish all this in a fraction of the time.

Many say Test-Driven Development doesn't work. It's too slow, and there's no value in doing it. Those words usually come from people who are either writing code for an "[obvious](https://en.wikipedia.org/wiki/Cynefin_framework)" domain or [don't know](https://en.wikipedia.org/wiki/Dunning%E2%80%93Kruger_effect) they're writing more code than what they need.

Jack didn't merely choose anybody to solve his problem.

He chose a professional programmer.

Thanks for reading. If you have some feedback, reach out to me on [Twitter](https://twitter.com/FagnerBrack), [Facebook](https://www.facebook.com/fagner.brack) or [Github](http://github.com/FagnerMartinsBrack).

Thanks to [Jay Bazuzi](http://jay.bazuzi.com/) and [Ian Tinsley](https://medium.com/u/c4ccb3ab8d17) for their insightful inputs to this post.

* * *