Why writing unit tests

Clément Bataille
5 min readAug 8, 2021

--

When I started as a software engineer, I didn’t write any tests. I was working on a new project in a startup, we were iterating a lot, and stakes were not too high. Historic was short, we were all aware of every part of the product, and bugs were not a problem.

Since then, I learned to appreciate unit tests, now that I’m working on the same codebase with 40+ engineers. With several years of historic, thousands of customers, and millions of users.

That may seem obvious to a seasoned software engineer, but let’s start from first principles and see why they are useful.

Photo by Israel Andrade on Unsplash

You are already writing tests

Let’s look at the basic development process, without unit tests. You will write code, let’s say a function, and manually test that this function returns the expected result. Depending on the complexity and the number of arguments, you may end up surrounding the function with console.log calls to test it.

function myFunctionToTest(myInput) {
console.log("input: ", myInput)
// do something console.log("output: ", myOutput)
return myOutput
}

This isn’t very different from a unit test.

But, with a unit test, you will be able to reuse this check, it will be shared with the team and future engineers, it is less fragile, less prone to errors (when you update your function, and forget one of the test case that is now broken).

Faster development process

Once you have a test setup, writing a test is not taking more time than checking manually the result of the function. It may take a bit more time to write it properly, but think of your future self not having to re-test every case.

Another advantage is that you can write and test functions, not having the whole project running. It’s easier and faster to launch unit tests than a project, that requires compiling, or building time.

Serves as documentation

Unit tests serve as documentation, in addition to good naming.

What does the function do? What do the parameters, the output, look like? What are the different cases, the different argument combinations? What are the edge cases?

All these questions could be tackled in simple unit tests.

Another use is when you want to write a comment. We try to avoid comments, but if there is something not clear you need to explain, oftentimes, it could be a unit test case.

Check the requirements

When you write your code, you have a list of requirements that need to be checked. It could be acceptance criteria, test cases, or any format. But at the end, the application must check all those boxes.

Writing exhaustive unit tests is the best way to ensure we cover all the requirements.

If they evolve over time, we can check the old requirements are still working. If we want to remove an old behavior, we do it consciously by removing the corresponding unit test.

If there is a functional question about something, and a unit test associated, anyone reading the code can confirm the intentionality in the behavior. It avoids the debate around “is it supposed to work like this”, and “is it a bug or a feature”.

It scales

When you develop a feature, you have all the context, you know all the use cases, but as soon as the project grows, the team grows, the list of requirements too. Unit tests ensure we control what the application does.

It also helps new joiners to better understand the codebase. When you are not sure about what a function does, having a list of comprehensive unit test cases is very helpful.

Helps write better code

By testing the code, you’ll write better code. Why?

Single-responsibility principle

Ideally, you’ll test a few test cases for each function. As soon as you realize you need to test more than a handful of test cases, it’s a good indicator the function is doing too much. In this case, you’ll want to split it in smaller functions that do less.

Separation of concerns

One thing you want to avoid, is having too many dependencies that need to be stubbed or mocked. That will lead you to write more decoupled code, keeping the dependencies in one place, or using other patterns, like dependency injections.

Pure functions

When you run a test, you don’t want side effects, or an output that depends on the number of times you run it. That’s why you will want to write pure functions (with no side effects).

Fewer bugs

We already talked about unit testing the requirements, meaning you will have fewer bugs for new features (requirements will be checked more robustly).

You will have fewer regressions, as the old requirements are encoded in unit tests, there is less chance a new feature breaks the existing behavior without you noticing.

And finally, there should be no regression on the same bug.

If you write only one type of tests, it should be regression tests.

Each time you find a reproducible bug (less easy for system bugs, like network errors), write a unit test! Ideally, the process of fixing the bug is: first, write a unit test that breaks, then, fix the code, finally, the test should pass. It ensures we understand what is the bug and how we are fixing it.

More refactor, cleaner code

Once you have unit tests, you can be pretty confident in refactoring your code.

If each time you want to refactor something, you fear breaking something, you’ll naturally not do it. Adapting existing code for new features will be harder, and you’ll move more slowly.

It can also be very useful while writing the code. Write a first version of the code that works while adding unit tests, to make sure all the cases are covered. Then, rewrite it focusing on readability, re-arrange it to have less coupling, etc.

How many times did you break something while cleaning code, during a code review for example, without noticing it? Unit tests should help to prevent that.

Move faster, deploy often

If you want to deploy often, you need automated tests.

You can’t reasonably re-test everything each time you make a change. That’s why if you don’t have robust automated tests, you’ll end up deploying once a week, or even less. Because you need someone to manually check the application extensively.

Writing tests is not enough

Writing unit tests is not enough. That’s the limit of some indicators like code coverage. Having tests doesn’t mean that your tests are useful and that you are testing the right thing.

You need good tests, you also need clean tests, they should help you identify bugs very easily. That’s why best practices are not the same in unit tests as in regular code. Typically, when a test fails, you don’t want to refer to many outside helpers to understand what is broken. It can lead to more duplication than in the regular codebase.

Then, you also need to run tests at the right time, having a standard CI/CD pipeline, the test suite should be fast (the faster the feedback loop, the better for the engineers), it should be reliable, and trustworthy.

--

--