The Hidden Benefit of Unit Tests — Code Quality Assessment

The Hidden Benefit of Unit Tests — Code Quality Assessment

It’s not all about reds and greens, you know?

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.

What does “poor design decisions” mean 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, this function is taking care of 4 different things? Such a function is not only hard to test, but also hard to maintain in general, thus becoming a terrible design decision.

Imagine having to test your log-in function, but instead of just checking credentials, this function takes care of:

  • Validating the user’s information to make sure it has the right format.
  • Send an email to notify the admin of your platform of a log-in attempt by this user.
  • Performs the actual credential validation.
  • Records the log-in attempt (and its result) in a database.

Four very different tasks make the function a very hard target for a test. How can you test only the validation logic without executing the rest of it? What if your test fails because it can’t send the email but you were just testing the credential validation logic? Does that make sense?

It doesn’t, does it?

Instead, 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 those functions’ code, they can go find it somewhere else, but if they don’t they can treat them as black boxes.

async function logIn(userData) {

  if(!checkUserDataFormat(userData)){
    retur false;
  }

  await notifyAdmin(userData)

  let credentialsValid = checkCredentials(userData.usrname, userData.pwd));


  recordLogInAttempt(userData, credentialsValid)
  return credentialsValid

}

Now that function is 15 lines long, and very easy to read. Mind you, it’s still doing several things, but now it’s only orchestrating them, the actual logic for those behaviors is somewhere else. Somewhere you can individually test in different test cases.

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 a function, 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.

Imagine if your tests were to fail because the 3rd party API is suddenly not responsive. Would you say there is something wrong with your code? Then why does your test have to fail?

The answer is: it doesn’t have to.

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? Is there a way for you to do that?

This is called Dependency Injection and the answer is no, there usually isn’t, so you have to either find a creative way of doing it or rewrite the function to allow for it. There is a caveat though: you’re probably changing a function that is being called from multiple files in your project, so maintaining backward compatibility with its previous version is usually a must. Either that or you have to change the way it’s being used across the entire source code.

Figuring out how to do this is going to depend on the language you’re using. Some dynamic languages like JavaScript allow for very flexible options here, such as using default parameters, or optional ones. Perhaps even overwriting require mechanics to replace external libraries with your mocked ones. But if you don’t have this flexibility at your disposal, a nice code-refactor might be the best approach.

Do not neglect to fix this problem due to the complexity of the refactor, just take it one step at a time.


If you liked what you’ve read so far, consider subscribing to my FREE newsletter “The rambling of an old developer” and get regular advice about the IT industry directly in your inbox


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.

For instance, take a look at the following code:

let op = Math.sqrt; //default value

function absoluteValue(num) {
  op = Math.abs;
  return op(num);
}

function squareRoot(num) {
  return op(num);
}

I know, it’s scary bad, but you can see the problem here: if you have a test for absoluteValue first, and then for squareRoot you’ll be getting the wrong result and you won’t know why until you check the actual implementation.

If instead, you turned the op variable into a parameter for each function, thus allowing you to decide what value they’ll have when called, you’ll be able to go back to that deterministic behavior we wanted:

function absoluteValue(num, op) {
  return op(num);
}

function squareRoot(num, op) {
  return op(num);
}

// you can then call them like this on your tests:

absoluteValue(-3, Math.abs);

As an added bonus, if for some reason you actually wanted to test what would happen if you called squareRoot with the Math.abs logic, you could do so in a deterministic manner.

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!

Did you find this article valuable?

Support The Rambling of an Old Developer by becoming a sponsor. Any amount is appreciated!