Are you a fan of unit tests?
I know I am!
It hasn’t been like that always though. I remember I used to hate them back then, but I eventually started realizing that there is more to them than just making my code stable. There is in fact, a hidden benefit that I haven’t seen mentioned anywhere:
Unit tests can highlight poor design decisions
This is a fact for the most common use case of unit tests: writing them on top of code that’s already been written without them. But if you think about it, practices such as TDD do have the effect of enforcing good design decisions as well on the code while it’s being written, otherwise you can’t really test it! So all in all, you could say that no matter when you apply your tests, you can reap the same benefit.
Now, what do I mean by “poor design decisions” exactly?
Think about it like this: unit tests are meant to help you isolate and test a single “unit” of your code. The meaning of “unit” varies depending on who you ask, but it tends to be related to a small portion of code that makes logical sense to be tested together. We have an easier time testing that code when we find it isolated in a function or method.
But there are ways to write these functions that are not really “user-friendly”, to say the least.
Extremely long functions
What happens when you go to test a function and realize it’s 250 lines long? And even worse, it’s doing 4 different things at the same time? Such a function is not only hard to test, but also hard to maintain in general, thus becoming a terrible design decision.
By splitting it up into multiple functions that only do one thing you can create an easier-to-understand logic, you simplify the task of whoever wants to interact with your function’s code by giving them mnemotechnic names that represent a block of logic. If they need to read their code, they can go find it somewhere else, but if they don’t they can treat them as black boxes.
And by sticking to the “single responsibility” principle, you can ensure that the logic inside your blocks is smaller and faster to understand. Mega-functions that do everything are never a good option, so try to avoid them at all costs!
Lack of dependency control
Some other times, it’s not about the length of the function. Sometimes you realize when you start working on it, that it depends on the response of different external services. I’m talking about 3rd party APIs, or even databases. Your unit tests are not meant to test the connection between your code and those services, that’s what integration tests are for.
What do you do then? You have to find a way to inject your own mocked (fake) services, but was this option considered during design? It usually isn’t, so you have to either find a creative way of doing it or rewrite the function to allow for it, while at the same time, maintaining backward compatibility with its previous version so you don’t have to change the way it’s being used across the entire source code.
Lack of deterministic logic
For you to be able to test a piece of code (whether it’s a function, a method or anything else) you have to ensure a very simple fact:
Every time you call that logic with the same input, you will receive the same output.
It sounds very simple, but you’d be surprised how many times I’ve seen code that relies on global variables. And considering how any other section of the logic could potentially affect those variables, the code you’re testing suddenly stops being deterministic. Because without changing the logic, and without varying the input, you can get a different output.
And if that is the case, how can you write a test that checks for that behavior? You can’t, or rather, you shouldn’t. Instead you should remove that global dependency into a local one.
Have you seen anything like this before? Or rather, have you experienced this particular benefit of unit testing?
I’d love to hear about other benefits to testing that you’ve found through your own experience, so feel free to reach out and share them with me!
In the meantime, have a great time coding!