The Ys of Unit tests - Making sense of a good practice Part 2
Obvious and not so obvious benefits of Unit Testing
Have you ever noticed that bugs have a mysterious attraction to your code? 🤔
It's like they have their own version of a “bug Tinder” 💘!
Swiping right on every line that looks vulnerable 🤷♂️.
But fear not my fellow developers!
In this article we are going to unveil the secret weapon that bugs dread the most: Unit Tests!
So, let's dive in and unleash the bug-repellent power of our coding arsenal!
In the first part we talked about testing in general as a practice and why we should test. We talked about how everything we write is a pure claim unless tested properly and that in order to move it from the claim column to the fact column it should be tested properly. In this part we will start with the first article about unit testing specifically and why you should write unit tests for your code. We will be talking about the benefits some that obvious for pretty much everyone and some are really not that obvious of Unit Testing.
What are the benefits of Unit tests?
Testing in general is beneficial obviously, integration tests gives us high confidence in the system but writing them is somewhat expensive and getting feedback from them is somewhat slower, end-to-end testing gives us even more confidence in the system interacting with different components or sub-systems but it’s even more expensive to create and maintain. The question that would arise is that: Are there benefits of using Unit Tests specifically? Well I am arguing in this article that the answer is a resounding YES.
But let’s see how it goes, let’s discuss the benefits one by one:
Unit test provides a sort of technical documentation
Surfing through the codebase by reading unit tests
Unit tests done right can actually provide a sort of technical documentation about your codebase. New joiners could easily read through the unit tests you have and understand what a service is doing by just understanding the assertions and reading the tests method names. Actually I have done the same in one of the companies I have worked for, they have written their unit tests so well that I could understand literally everything in the codebase, there was no need (most of the times) to ask questions, because the answers are documented actually in the unit tests themselves. It was so hilarious that I could also makes sense of certain changes and certain history of changes in the codebase by looking at unit tests removed/refactored/added.
The unit tests could provide such valuable assets, that’s providing technical documentation about your codebase through:
Test Naming: The names of unit tests can often provide insight into what the function or module being tested does. By using clear and descriptive names for the tests, you can get an idea of what the code is intended to do without having to read through the entire codebase.
Test Cases: Unit tests can also provide examples of how to use the code in different scenarios. By creating a suite of test cases that cover different inputs and expected outputs, you can refer to these tests as examples of how the code should behave in specific scenarios.
Test Assertions: The assertions made in unit tests can provide insights into the expected behavior of the code. By analyzing the assertions made in the tests, you can gain a better understanding of what the code is intended to do.
Code Changes: Unit tests can document code changes over time. By reviewing the history of the unit tests, you can see how the code has changed over time and how those changes have impacted the functionality of the code and making sense of them.
Unit tests are powerful tools in the toolbox, not just for verifying things and asserting claims. Not just to act as safety net for you. But also to provide documentation about your codebase. Have you ever imagined that some people out there use unit tests as a mean to make onboarding new people in the team easier? Isn’t that great?
Embracing the Change as an inherit nature of Software
There is only one guarantee in software, nah it’s not availability, it’s that they change, a loooot
It’s almost always the case that software aren’t immutable, they change all the times. The change can range anywhere from a simple new feature, a minor requirement change, a teeny-tiny slight misunderstanding of the requirement, domain expansion, infrastructure change, new non-functional requirement, a vulnerability patch, a new update because…. because we can why not! 😏 😏😏
all the way to the catastrophic changes such as (Oh wait, we have built a solution for a completely different problem)-kind-of-thing; or some times due to (a cascading catastrophic change in the requirement in a massive enterprise due to a tectonic movements in the sub-domains as a result of what I also call “Initiatives fever”).
Initiatives fever: It’s a phenomenon appears in massive enterprises where everybody everywhere all the times want to start all kind of initiatives, the one who start the biggest initiative wins.
A careful reader: OK, so Software changes, now what? What can we set against the fact that software change to reduce the impact of it?
Unit tests are your safety net, the cheapest, quickest, easiest to read easiest to maintain safety net against the fact that Software changes.
Because unit tests verify claims about your software at such a low level, software that change means a change in the claim, and since we wrote our unit tests with verifiability and falsifiability in mind (remember our point above?) unit test would fail and they would grab your attention about the fact that something has changed.
This is unbelievably powerful weapon since you can verify that changes anywhere didn’t break something somewhere and that can be easily automated and it’s so cheap and easy to automate that you can run them each time anybody change anything anywhere (e.g. CI/CD pipeline that’s triggered on feature branches as well).
A careful reader: But unit tests mocks everything else, they are test in a complete isolation. They won’t catch for example integration issues, or the issues that might pops up when multiple units work together.
Yes that’s absolutely true.
I didn’t say we should not use other kind of tests (back to this later) what I am saying or rather trying to make sense here is specifically unit tests.
A careful reader: Forget about faster feedback, forget about cheapness. Is there anything that unit tests can do, that integration tests can’t?
I would say YES. Integration tests have a certain scope to them where they verify “big” business related flow, they assert that’s something happened because of a certain interaction. But they cannot for example verify the following “If another dependency throws an exception that exception should be handled within that unit gracefully”. How would you do that in integration tests? For example let’s imagine that you are calling the database and a certain “Command Exception” for some reason must be handled gracefully “Swallowed” or not propagated up the calling stack how would you verify that? With unit tests it can be easily done by mocking the dependency and then in the setup stage you setup the method to throw `Command Exception` and you verify in the verification stage that your code is not propagating up that particular exception type. Unit tests basically looks at your code with a such magnifying powers that certain things are only possible with them.
Let’s conclude this point as well:
Software always change, they on 99.9% of cases aren’t immutable
What we can set against that fact are Unit Tests
While integration tests are also great tool in the box, that doesn’t mean we can use them as substitution or instead of unit tests. To each strengths and weaknesses, pros and cons. And we should strive to use them both
It’s easier to strive for 100% coverage with unit tests
By achieving 100% coverage with unit tests, it’s easier now to know what kind of integration tests you need, specifically targeting the integrations points that are left unchecked by unit tests, as consequence you become more conscious about what kind of integration tests do you need and thus your testing strategy become more efficient.
Why Unit Testing? Why not other types of tests?
The mess that’s testing types
There are all kind of test and people are still naming new testing types to this day. So the question now is why not focusing on other kind of tests like for example we could go all the way e2e or integration tests, or 20% e2e and 80% integration tests. Maybe instead of unit testing that usually use mocking (or other kind of ..) we could go for the so called “sociable unit tests” where instead of mocking (or whatever) the dependencies you actually instantiate the dependencies (to a certain level that they won’t become integration tests) and test the chain in kind of integration testing manner but without the integration part really, just testing services in integration but not for example database calls or 3rd party services calls.
First of all let’s take a look at the famous “Testing Pyramid”
The upper you go in the “pyramid” the more expensive the test both to write and to maintain, the less “isolated” they are but the “higher” the confidence they give you in the system from the business perspective and vice versa. So it’s more expensive for you and your team to go all the way “manual testing” and it cannot be automated “hence the manual part” but as they say “seeing is believing” it’s high in confidence hence you tested it yourself, right? Integration and e2e testing are less expensive than manual testing and can be automated and they gave you decent amount of confidence in the system but they are very expensive to write and to maintain. The component test are the “sociable unit tests”.
IMHO, the difference between these layers is the “magnifying lens” they use. Unit tests looks at the code with the highest magnifying lens, you are looking at line of codes “paths if you will” and you are asserting certain behaviour for each path. But something like E2E testing doesn’t have as much “magnifying powers” so it looks at the system as “components” interacting with each other and it asserts that the result of such interactions have valid response/value from business perspective.
From business perspective, if the user wants to create an order and order must be validated and if valid should be created; from business perspective there can never be such a thing as “null” order right? Here where the “magnifying powers” of unit test shines.
For example, especially if the e2e tests are “derived” by legitime business scenarios/use cases and especially if the people writing those scenarios are business people nobody will say “hey let’s test that if the order is null the system should handle that somehow gracefully”.
So instead of having this dangerous possibility go unchecked and not handled gracefully a very cheap and simple unit test could help us verifying that we either throw an exception in case order is null or return false or whatever in matter of couple milliseconds.
That magnifying lens of unit tests will make our code more reliable, it will make it less likely to have simple but dangerous things like this go unchecked, it’s very cheap to write, it can be automated, it’s easy to read, easy to reason about and it’s very cheap to maintain. If someone somehow removed the line that checks the null-ability of the incoming order and if that somehow slipped through the code-review process, that unit test will fail and as a result the CI/CD pipeline will also fail.
The main point I wanted to make from this paragraph is the important concept of the “magnifying lens” that the unit tests have. Thanks to that magnifying lens that inspects your code “line by line”, it sets your mindset in a certain way, it forces you to think in certain ways and it promotes better thinking about the code you are writing. Please keep in mind that concept because we are going to use it a lot from now on.
With that point, I’d like to end this article here. I am trying to make those articles “short and sweet” as they say so no one get bored while reading 😉 not so many people like to read especially nowadays 😓, where people attention span has become the shortest ever.
In the next article I am going to talk about even more important benefits of unit tests and how they for example guide the design of the code and promote best practices and so on and so forth (let’s not get ahead of ourselves 😉). I’ll also try to make it as technology agnostic as possible (not just C# or .NET thing).
I’d appreciate the interactions, if you think anything could be improved or you would like to start a discussion please feel free to write a comment. I am hopping to build a community here not just writing my personal thoughts and experience.
Unit testing: Because life is too short to manually debug the universe
Thanks again for reaching this far and please stay tuned for more and a lot more, not just about unit test but a lot more.
Nice article! And interesting phrase “sociable unit tests”. I didnt hear/see it before.
Tests are like code-stimulators + diagnostic in one frame - ensure that code is alive and healthy :)