Rafael dos Santos Miguel Filho
The Pragmatic Engineer

Follow

The Pragmatic Engineer

Follow

Taking Control of Software Entropy: How Unit Testing Can Make the Difference?

Software entropy is the tendency of software to degrade over time as it is modified. - Ivar Jacobson

Rafael dos Santos Miguel Filho's photo
Rafael dos Santos Miguel Filho
ยทFeb 27, 2023
Taking Control of Software Entropy: How Unit Testing Can Make the Difference?
Play this article

Table of contents

  • The true purpose of unit testing
  • Why is entropy in software so bad?
  • Tests can help control entropy?
  • The uncertainty of modifying
  • Entropy in Enterprise Applications
  • Conclusion

Yes, that topic again. After all, we all know that unit tests are essential, right? Apparently not... I remember two years ago I heard from a software project manager his opinion against testing; he didn't understand why testing took so long, and that it was a delay in final deliveries. Is this less recurrent? I believe not, especially in smaller companies. Perhaps this resistance even comes from the company's board of directors. Unit testing does not always receive adequate appreciation in the software development process. Often, the focus is directed to other areas, such as user interface design or speed of delivery, which can end up creating a corporate culture that doesn't recognize the importance of unit testing. What I will try to show in this post is how testing can prevent entropy in software. So let's start by better understanding the purpose of unit testing.

The true purpose of unit testing

The main purpose of unit testing is to ensure the healthy growth of your software project. As the project grows, the number of codes increases, thus increasing the surface of possible errors in the code. In other words, the more code you write, the greater the chance of introducing a bug. The unit tests allow you to detect these errors early and fix them effectively, thus ensuring the long-term quality and stability of the project.

You may be wondering, can't we do manual testing? Manual testing, as the complexity of the software grows, is inefficient and prevents the agile release of new functionality to users or customers. But this is not the case with automated tests, especially since their execution time is expected to be very fast. Unit tests allow us to detect errors as early as possible in the development process, which makes it easier and cheaper to fix them.

But how can automated testing help us control and fight against increasing clutter within the software? First, we need to understand what this entropy means. In the book Unit Testing Principles, Practices, and Patterns, the author comments on exactly this subject, and we will understand more in the next topic.

Why is entropy in software so bad?

First, let's understand what is entropy:

In physics, entropy is associated with the second law of thermodynamics, which states that in a closed and isolated system, disorganization cannot decrease, but can remain constant or increase. The notion of software entropy, which was first presented in the book Object-Oriented Software Engineering, was influenced by the scientific definition. In essence, the more a software system changes, the greater its disorganization becomes, that is, the entropy always increases. We can use a simple analogy to better visualize entropy in a system:

Imagine a bookstore that over time grew fast. The addition of new books did not follow clear criteria, some shelves became overcrowded while others remained almost empty. In this bookstore, there was no separation of books by category. Consequently, the environment became cluttered, making it difficult to find the books that customers wanted. The employees are forced to spend a lot of time looking for specific books and sometimes even asking the customers for help. Reorganizing the books becomes an extremely complex and time-consuming task, and there is always the risk that books will get lost or misplaced. To make matters worse, customers start to see the backlog and give up buying books. Thus, sales drop dramatically!

Illustration of chaos caused by entropy in software.

It is now clearer to understand in the context of software what entropy can cause. When a software project is started, everything is organized and planned carefully. But as the project evolves, more features are added, and the code becomes more and more complex and difficult to maintain. The disorganization increases and it becomes more and more difficult to understand how everything fits together.

If entropy in software is not controlled, the complexity of the project can increase so much that deadlines can be missed. Development becomes slower, errors are more easily introduced, and modifications to the code become increasingly difficult. It is therefore critical to deal with disorganization in a software project, to keep complexity under control, and to ensure that deliveries are made quickly and efficiently.

So how can we avoid reaching this irreversible state? By writing quality automated tests! See the next topic.

Tests can help control entropy?

Yes, because with tests we avoid regressions! A regression means the introduction of unexpected behavior or behavior that would have already been corrected in other deliveries.

Software regression can be a consequence of software entropy, because as entropy increases, the probability of introducing defects into the code also increases, which can lead to software regression.

Regressions occur mostly when new code is added or refactorings are performed. In short, regression is a big problem! As we have already seen entropy is the chaos, the disorder within a software. To combat it, unit tests must seek to verify units of behavior. This ensures that regressions do not occur so frequently! Besides this, unit tests help in other fundamental areas:

  • Secure Refactoring: Unit tests can be used to ensure that code refactoring is performed safely. This is because unit tests isolate functionality, and any problems detected after refactoring can be easily identified and fixed.

  • Improved code quality: The test encourages developers to write more modular and cohesive code, which reduces system complexity and makes it easier to maintain and add new functionality.

  • Early problem identification: Unit tests help identify problems in code at an early stage of development. This allows developers to solve problems while they are still easy to fix and prevent these problems from spreading to other parts of the code.

  • Higher Productivity: Unit test implementation can increase development team productivity by allowing developers to focus on writing new features and improving code, rather than spending time troubleshooting problems.

So, building unit tests for each piece of code as it is added enables programmers to detect and fix problems as they appear, preventing them from accumulating. In addition, well-designed unit tests can help ensure that changes made in one part of the code do not have side effects, helping to maintain the cohesion and organization of the software. See the code below:

decimal note = 85.25M;
string letterNote = " ";

if (note >= 90.0M)
{
    letterNote = "A";
}
else if (grade >= 80.0M && grade < 90.0M)
{
    decimal noteRounded note = Math.Round(note, 1);
    if (noteArround >= 80.8M)
    {
        letterNote = "A";
    }
    else
    {
        letterNote = "B";
    }
}
else if (note >= 70.0M && note < 80.0M)
{
    letterNote = "C";
}
else if (grade >= 60.0M && grade < 70.0M)
{
    letterNote = "D";
}
else
{
    letterNote = "F";
}

Console.WriteLine($"The letter of the note {note} is {letterNote}");

The above code has no unit tests. Would you feel comfortable refactoring it for improvements and even adding new features? Without tests, it is much less safe to make changes and refactor. In this case, the code is not that complex, it is easy to understand what it does, but it is not bug-free. But in the corporate world, the complexity is much higher! The goal of the example above is to show that the messier a code is, the harder it becomes to change it quickly. Furthermore, we can list some reasons why refactoring or adding new functionality to code that has no tests can be frustrating:

  • Risk of introducing new bugs: When making changes to code without proper unit tests, there is a risk of introducing new bugs, because there is no guarantee that the changes will not negatively affect other parts of the code.

  • Breaking functionality: During the code refactoring process, there can be a break in existing functionality, without the problem being identified immediately. Without unit tests, it becomes more difficult to determine the cause of the problem.

  • Difficulty assessing the impact of changes: Without unit tests, it is difficult to know what impact changes will have on other parts of the code. This can lead to integration problems and unexpected system behavior.

Okay, we understand that trying to change code without testing is like walking in the dark, but let's go deeper into the subject. Let's talk about uncertainty.

The uncertainty of modifying

Let's look up the word entropy and understand more about its meaning in the dictionary:

entropy: the degree of disorder or uncertainty in a system

I like the word uncertainty, especially in the context of software engineering. We know that we can't always be 100% sure about functionality or understand 100% of the entire flow of the application. There is always a gap, which is uncertainty. Especially when we are working in development teams, we have many people, with different degrees of knowledge and experience. The uncertainty increases even more! We can make another analogy to better understand what I want to explain (you may have noticed that I like analogies):

Imagine a team of climbers planning to climb a mountain. They know that the mountain has a challenging route, but they are not sure what they will find along the way. They prepare as best they can by studying, mapping, and gathering information about the climbing site, but still, there is a lot of uncertainty. As they start climbing, unexpected obstacles may arise, such as loose rocks. The team needs to be able to adapt quickly and find solutions to problems that arise along the way. If the path becomes too uncertain and dangerous, they may even need to backtrack and find another route. Then it is clear that the climb is becoming uncertain and dangerous, and is taking up a lot of the team's time.

The same can happen with software teams. The higher the degree of uncertainty, the more time it takes to change the code. The problem is that time is a big enemy for software teams and managers, and this can compromise deadlines and even contracts. So how to reduce the uncertainty that a code brings? Decreasing the uncertainty of an algorithm is only possible with well-written and reliable tests. The absence of unit tests in an algorithm can generate uncertainty for the development team because there is no consistent assurance that the changes made in the code will not harm the existing functionality. Without unit tests, understanding how the code operates and the consequences of the changes made can be complicated. The result can include regression problems, where a change that seemed harmless ends up hurting functionality that previously worked correctly. In addition, the lack of unit testing can undermine the team's confidence in the code, causing a slower development process.

We should value testing more, this is the message I am trying to get across. As I've said in other articles I've written, silly bugs or behaviors that didn't comply with functional requirements could have been stopped before going to production if there was a culture of writing tests. Finally, see the code below:

public decimal CalculateTaxRate(decimal totalValue, string country)
    {
        decimal tax = 0;

        switch(country)
        {
            case "Brazil"
                tax = 0.10m;
                break;
            case "USA":
                tax = 0.05m;
                break;
            case "Canada":
                tax = 0.15m;
                break;
            default:
                throw new ArgumentException("Invalid Country");
        }

        decimal tribute = Math.Round(totalValue * tax, 2);
        decimal result = totalValue + tribute;

        return result;
    }

If the developer who created this method took the time to develop the unit tests, changing, refactoring, and adding new functionality or validations will be much easier:

     [Theory]
     [InlineData(100, "Brazil", 110)]
     [InlineData(150.50, "Brazil", 165.55)]
     [InlineData(100, "USA", 105)]
     [InlineData(100, "Canada", 115)]
     public void TestCalculateTaxRate(decimal totalValue, string country, decimal expectedResult)
     {
         // Act
         var result = CalculateTaxRate(totalValue, country);

         // Assert
         Assert.Equal(expectedResult, result);
     }

How long do you think the developer took to create this test? Obviously, there may be others as well. But I believe a test like this can be written in less than ten minutes. The great advantage of starting the tests from the beginning of the project is that everything at the beginning is easier to change and code. Mainly unit tests. The behaviors are smaller and that costs little time! That's why I emphasize once again, start the project the right way, and write tests, most likely they will add a lot of value in the future!

By writing tests from the beginning, we decrease the uncertainties of the future.

Entropy in Enterprise Applications

Most developers who have worked for large corporations know that the business rules in enterprise applications can change at any time or be adjusted with every new feature that is added.

The degree of the disorder tends to always increase. Why? Because the more changes are requested, the more the domain and rules get specific to solve the business problems. Entropy increases not only because we do not follow good development practices, but also because of the high complexity of the application and the problems it tries to solve. We can cite an example of the Voucher class that provides discounts on products.

If there is no correct validation for the expiration date of a discount coupon, this can allow users to use the coupon after the expiration date, resulting in reduced sales or financial losses for the company. This can happen in several ways, such as:

  • A user may share an expired coupon with other users, who may try to use it, but the system does not check the expiration date.

  • The customer can save an expired coupon and try to use it again later, without realizing that the expiration date has passed.

  • A user may use an expired coupon, but the system does not register that it should not have been accepted, allowing the same coupon to be used again by other users.

These scenarios can result in significant financial losses for the company as users may get unauthorized or unplanned discounts, reducing the company's revenue or affecting the company's profit margin.

But let's assume that now the Voucher class has been given new requirements, for example, there are now discount coupons for product categories such as electronics, clothing, and many others categories. Also, each coupon for different categories will have different expiration times set. An example of this might be a coupon for the electronics category, which expires in 2 days. And each category has different expiration dates. See the requirements have changed a lot, the final behavior of the Voucher is still to provide discounts on top of products, but now there is more complexity generated. If the domain does not have adequate unit tests to verify that these behaviors occur properly, this will be a big problem! But when we have reliable tests that are practically the documentation of the application, that have descriptive and clear names, and their internal structure is readable and well-designed to make it easy for any developer to read, it certainly becomes easier to understand how to adapt the existing code through testing. What I want to convey is that in large corporate applications of high complexity, we will always have to make changes that we don't expect, to avoid regressions and chaos in the software, it is crucial to always ensure good readability and take care of the quality of the tests.

But remember that we will never be able to make software entropy free, it will always exist, what we can do with the help of unit testing is to try to control this disorder and also reduce it as much as possible. I will leave a sentence that I don't remember who said, but I read it in a book and I haven't found the author to give the proper credit:

Software entropy cannot be eliminated, only postponed. - Unknown Author

Conclusion

Perfect software does not exist; software entropy will always be part of our work. We will always need to fight to control entropy, we can't decrease it, but having good unit tests and integration tests too, can help to control and avoid chaos within the software. So look for ways to always improve the way you think and write your tests. Try to learn from others and pass this knowledge on. Maybe then entropy can be controlled as the software grows and your work will be less stressful.

Throughout the article, we have seen several points on how entropy occurs and how unit testing can help. They are not the solution and do not stop entropy! But they are useful to help control entropy. There are points that I consider fundamental to help control entropy in software and they are related to testing:

  • Safer refactoring

  • Ensuring expected behavior

  • Avoid regressions

  • Decreases uncertainty

  • Adding functionality becomes less unsafe

Other aspects could be on this list, but I prefer to keep those. Thank you very much for reading to the end! If you liked what you read, please share! See you next time! ๐Ÿ˜‰

ย 
Share this