We started this small-ish series with Part 1 talking about testing in general as a practice, why should we test? What’s the reason behind it and why it’s so important. Then in Part 2 we started focusing on the practice of unit testing where we mentioned some benefits of Unit Tests and we introduced a very important concept (The magnifying lens of the Unit Test that inspect your code in details). I would like to finalize this series with this Part 3 of the series of “The Why(s) of Unit Tests - Making sense of a good practice” by talking about more benefits of Unit Tests and some history lessons.
I will use C# code in this example for the sake of “speediness” but I promise I am going to (at some point in the future 🤫) modify the articles and make them as technology agnostic as possible, in the end, I am a big fan of C++, Golang and Rust too😉 I am pretty sure the principles still applies everywhere so please 🐻 with me a bit if you stumbled upon a C# code.
Unit tests promote verities of good practices
How does unit tests make your code cleaner
While we could argue till morning next day what a clean code is and whether we should question every good practice in the industry or not; or whether we are engaging in “cargo-culting” or not (Apparently this is the new thing in the industry where you Eww the best practices and label them “Cargo culting” 🤷♂️). BUT! Let’s just agree on few very simple things shall we?
Clean code is a code that’s easy to read, test, maintain and deploy ✍️
Good practice is a practice that help us do the point above 🤝
So when it comes to unit testing how does that apply?
Remember my point in Part 2 about the magnifying lens for unit tests? By having a magnifying lens in such low resolution/max zoom in, you are forced to incorporate thinking about the code design and structure in order to be able to write unit tests. Specifically, three points, you will have to consider in order to write unit tests:
Code Modularity: Unit testing encourages the practice of writing modular code that can be easily tested in isolation. By breaking down a large, complex system into smaller, more manageable units, you are effectively making the complex less complicated and easier to reason about. Code modularity is a good practice, let’s agree one that? Don’t you think it’s easier to reason about smaller portions of code that’s reasonably and properly isolated rather than the whole thing? By writing unit tests for the code you write; you are forced to think, ahead of time, how is the code going to be structured and modularized.
Code Design: When writing unit tests, you are also forced to think about the design of the code; and how different units of code should interact with each other. This encourages you to write code that is easier to understand, maintain, and extend. Principles like SOLID for example are good practices (Let’s just agree on that?). And actually by writing unit tests you will detect any code smell. Unit tests that are well written help you identify code-smell, for example if you see that unit test is actually verifying too many things then perhaps the unit under test is doing too many things and thus violating the SRP (Single responsibility principle) or if you see that a unit test class has too many tests perhaps that service under test is doing too many things (Maybe a segregation of interface here could help? For example instead of one almighty “OrderService” you could segregate the service into smaller ones for example “CreateOrderService”, “DeleteOrderService” and one “OrderService” for all reads operations?)
Code Refactoring: Unit testing makes it easier to refactor code without introducing bugs. By having a suite of tests that can be run after each change, you can be confident that the changes didn’t break existing functionality. It’s especially the case since unit-tests are with such “magnifying lens”, refactoring is not necessarily changing anything business wise but could be sometimes removing one liner that “supposedly does nothing”, or maybe moving the logging functionality somewhere else, throwing exception in the next line instead, changing an if condition. All these changes or refactoring are very miniscule that they could in principle introduce bugs with them that slips through the integration or e2e tests. That’s exactly why unit tests are so powerful.
That’s why practices like TDD (Test driven development) are really great! When you start with the testing and let the test lead the code you also start (early on) thinking about how that code should look like in order for me to write an easy to read, easy to reason about Unit Test.
There are a lot of things that you would automatically be mindful about once you incorporate that way of thinking, thinking from the perspective of Unit Tests. For example how many of us when wanting to refer to the current time/date just basically used the static property DateTime.Now
in C#
for example?
A careful reader: WAT, is DateTime.Now
a bad thing now?
Could be, especially when you are using DateTime.Now
in +1000 place and now you discovered that, oh shoot I must have used DateTime.UtcNow
instead 🤔 imagine the pain of checking +100 place for the correct use/fetch of the current time 🥲 also imagine the horror of making sure you and your (+I don’t know how many) team are using either DateTime.Now
or DateTime.UtcNow
everywhere all the times, just imagine the nightmare of having date values in some timezones in certain area in your database and in a completely different timezones in another areas, you would wish that only if you would have done a better code review, only if you had not taken that vacation at that time right? PAIN!
Imagine now that you need to write a unit test, actually let’s take a concrete example in C#.
Take careful look at the highlighted line, how do we make sure every time all the times, after every single refactoring, after every single code change to that code that we are always no matter what, using the actual factual real current date/time value in UTC as a value for CreationDateUtc
property in the entity before saving the entity in the database?
I’ll let you think about it for 10 minutes…
OK, Times up!
This is called the ambient context anti-pattern
and it’s an anti-pattern for a reason. I’ll expand on that in a different (micro-small series of articles promise 👌).
The only way to Unit Test that part of the code is to somehow “setup” the public static property UtcNow
of the DateTime
class with a specific value and then after the call verify that, the code has actually set that exact value.
One thing we could do is:
Let’s create an interface/service that hides the public static property behind it
Let inject that service as a dependency in our
CustomerService
Let’s use that service instead
Finally let’s add a unit test for that line
Run 💚
What did we achieve with that very simple unit test:
We made sure that the code is correct, it’s doing what it said is doing and ensured the correctness of that seemingly minor functionality of setting the creation time
We formalized/standardized the way we access the
DateTime
API, you are no longer exposed to a whole set of functionality and choices in C# case for example you don’t have to chose betweenDateTime.UtcNow
,DateTime.Now.ToUniversalTime()
,TimeZoneInfo.ConvertTime()
,DateTime.Specifykind
, etc. 🤯We made our code cleaner, we no longer relis on a static field!
There are still much much more benefits of using Unit Testing, from ensuring certain performance metrics in a hot-path in your code, to automating the enforcement/standardization of some very complicated coding-style/API-access to simplifying your upper layer tests (Integration/E2E tests) by pushing some simple bug fixed downwards towards the Unit Test layer (remember the Testing pyramid?) etc. etc. But I am thinking let’s end this series here for now I think we covered the most important points related to the topic.
Finally: Why I as an Engineer should write unit tests?
Professionalism and the immense responsibilities of Software Engineers
After all what we have discussed so far I think it’s safe to conclude that unit tests are really crucial and they play a major role in verifying that what we wrote works, protect us against mistakes and bugs and embrace the change as inherit nature of software and so on and so forth. But then, I think the question still hold: why would you want to write unit test? In the end it’s more things to do. And one can assume that we as Software Engineers do our best to provide/contribute the most while striving to do less (efficiency).
Well, let me ask you this question: Why did you become a Software Engineer? Software Engineering is not fun all the times
Sometimes Software Engineering can be really boring, if you really believe that Software Engineering is all about cool stuff and fun things all the times, then perhaps you should re-consider your thoughts again. There could be a loooot of repetitive things, you could be maintaining a supermassive old-as-heck giant monolith legacy-as-56k-modem codebase. I mean what’s interesting about trying to make sense of other people code right? Especially if the codebase for a system that doesn’t really do much besides the basic CRUD stuff (create, replace, update, delete).
Sometimes Software Engineering can be really exhausting, are you an introvert who have very limited to non-existing capacity for social interactions? Or perhaps you communicate with other people like a robot that some auto-generated subtitles in a random YouTube video can yield better communication lines than you? Or maybe you just need “re-charge” after some prolonged meeting, discussion, small-talks, etc…? Well, guess what! One of the most important things in Software Engineering is team work, collaborations and communication with others. In the end:
No man is an island, entire of itself; every man is a piece of the continent, a part of the main.
There could be a lot of people you need to talk to before even writing a single line of code. In some Agile practices, requirements goes through a lot of steps before becoming technical tickets that you as Engineer/Developer can start writing code to implements (Like pre-refinements, refinements, and all sort of ceremonies around them).
Basically Software Engineering is not for everyone!!! It requires a set of unique skills, creative but logical thinking, a deep knowledge on varieties of domain and willingness to know and to discover, immense curiosity, a certain temperament, flexible but quite disciplined mindset, a wanderer mind if you will! It require deep understanding of Mathematics, Computer Science, Algorithms, Data structure and Engineering principles. If you struggle with any of these you might find yourself struggling to keep up with the demand of the profession. It requires a significant amount of problem solving skills, you need to keep your mind fresh by solving problems on websites like Leetcode, Hackerrank and others, you need to keep up with the rapidly-changing technologies, for example each year there are new versions of C# and .NET framework, new things are added to the C++ STL, new technologies are founded and created, a new tool must be added to the toolbox.
Software Engineering as a profession is very impactful, it has always been that way throughout the human history since the first bit of bytes written and shipped, it is nowadays and it will always, always be one of the most if not the most impactful profession in the history of the human kind if not the whole universe or all parallel universes for that matter.
Whether you like it or not, you as Software Engineer, set with yourself a whole set of expectations. Whether you like it or not, you as Software Engineer has an impact in the world. Your work affects others. So you have to act professionally. There is no other choice. Let me remind you of some history lessons:
The Therac-25 incident: The Therac-25 was a radiation therapy machine used in the 1980s and 1990s to treat cancer patients. Due to a programming error, the machine delivered lethal doses of radiation to several patients, causing serious injuries and deaths.
The London Ambulance Service incident: In 1992, the London Ambulance Service introduced a new computer-aided dispatch system to “improve” response times. However, the system suffered from a software error that caused delays and miscommunications between the dispatchers and ambulance crews. As a result, several patients died or suffered serious harm due to delayed medical attention.
The Mars Climate Orbiter incident: In 1999, NASA's Mars Climate Orbiter was lost in space due to a software error in the navigation system. The software was designed to convert measurements taken in pounds into the metric unit of force, Newtons. However, one team used the pounds measurement in its calculations, while the other team expected the data to be in Newtons, causing the spacecraft to approach Mars at the wrong angle and ultimately leading to its destruction.
The Ariane 5 incident: In 1996, the maiden flight of the Ariane 5 rocket failed just 37 seconds after launch. The cause of the failure was traced to a software error in the rocket's guidance system. The error was caused by a data conversion issue between two different systems that used different units of measurement.
The Toyota Acceleration incident: In 2009, Toyota recalled millions of vehicles due to reports of unintended acceleration. The problem was initially blamed on faulty floor mats, but further investigation revealed that a software glitch in the electronic throttle control system could cause the accelerator to become stuck.
The Sago Mine Disaster: In 2006, an explosion at the Sago Mine in West Virginia killed 12 miners and injured another. The cause of the explosion was traced to a lack of proper gas monitoring, which was due in part to a malfunctioning communication system that relied on outdated software.
The Heartbleed Bug: The Heartbleed bug was a serious security vulnerability that was discovered in the OpenSSL cryptographic software library in 2014. The bug allowed attackers to steal sensitive information, including passwords and encryption keys, from affected websites and servers. While the bug did not directly cause any deaths, it had the potential to be exploited by malicious actors for deadly purposes.
The Boeing 787 Dreamliner: In 2013, a battery on a Japan Airlines Boeing 787 Dreamliner caught fire while the plane was parked at Boston Logan International Airport. The incident was caused by a software error in the battery management system, which resulted in an overcharging of the lithium-ion battery.
As you can see most if not all of the problems above could have been easily avoided by testing, and actually some of them could have been prevented by an easy and quick unit-test.
The practice of Unit tests is a professional responsibility because you cannot submit your code or contribution to the codebase without making sure it works and as we already discussed the only way to do that is to somehow scientifically proving that it actually works. And that’s where the tests comes to play! But again you can and should do other kind of testing but unit tests still is and will always be a great practice that you should never forsake.
I hope by now you appreciate and understand why unit testing is really important and I hope that I was able to help people out there making sense of a good practice that’s unit testing and testing in general.
And to all the careful readers out there: Thanks for reading this article as a whole and not skipping any paragraph. I would like to hear from you, what do you think and would very much like to have engaging conversations in the comment section of this article so please feel free to write your thoughts down there and feel free to critic or question any part of this article that you might disagree with.
Nice article, thanks! Term/comparison "magnifying lens" - 100% match :)
About IClock interface/approach: maybe it is better to pass current date/time as a parameter into domain service?
In this case we will have less dependencies in domain service. As a result - it should be easier to test + we will have more explicit method's signature/contract.