The Good Unit Test

Team Merlin
Government Digital Products, Singapore
6 min readSep 25, 2020

Unit testing is the most foundational form of testing. As the name suggests, the units of source code are tested like functions and modules and thereby ensuring that they perform according to the specified behaviour. Regardless of the variations of the testing pyramid you encounter, they are at the bottom and should account for the majority of your testing efforts.

Testing Pyramid by Martin Fowler

Why unit tests?

It is clear why testing, in general, is important. However, why write unit tests instead of other types of testing like integration, E2E, API, and UI tests

Assuming unit testing is done well, here are some key reasons:

  1. It encourages developers to think through the design of the features as they (or even before) write their code at a modular level.
  2. It is a cheap and fast way of verifying the design contract of your functionality.
  3. It provides additional/alternative documentation in understanding your application’s behaviour.

How can we make a good unit test?

What Happens After You Execute a Good Unit Test

Now that you’re onboard with unit tests, what exactly makes a good test? Here are some traits that make the unit test a good one:

Readable Tests === Maintainable Tests

Test code is, for better or for worse, a lot like production code. The more readable it is, the more maintainable it becomes. If you’re wondering if your test code is readable, ask a new guy or another teammate to take a look. If it makes their brain hurt or they take ages, then you should consider refactoring it.

Reliable Tests === Peace Of Mind and Clarity

Unit tests should unearth bugs in your system and should ONLY fail when they do so. What this means is that when I run the tests twice or n times in a row, it should always return the same results.

Take for example this piece of code and the unit test for it:

Do you think this is a good unit test? Why or why not?

In our opinion, this is a bad test as it will not pass/fail consistently since it depends on the time when the test is being run, thus making it unreliable. Other alternative examples include API calls or code which rely on some form of setTimeout.

Independent Tests === MORE Maintainable Tests

Ever check out a fresh copy of the source code and encounter difficulties running your tests without an immense amount of set-up? Or get blocked by a situation where you pass a full suite of tests but fail hard at running a single test?

These are typically signs of interdependent tests. The biggest of them is the decrease in velocity and developer frustration. In the long run, it will become harder to write and maintain tests.

Here are some sources of it:

Signs of an Interdependent Test by Lasse Koskela, the author of Effective Unit Test

If you really cannot avoid the hassle of setup, make it easy for the developers to set it up once in the beginning.

Fast Tests === Shorter Feedback Loop

This is very self-explanatory. To quote Lasse Koskela, the author of Effective Unit Test, a long delay in feedback is a real productivity killer. It brings development to a halt when the developers are waiting for the test to complete.

Feedback Loop by Lasse Koskela, the author of Effective Unit Test

So, keep your tests fast. How fast? Well Michael Feathers, in Working Effectively With Legacy Code, states that a “unit test that takes 1/10th of a second to run is a slow unit test.” Hence this could be a place to start.

What can I do to get started?!

Looks like we’re ready to go!

Caveat: We’ll be using Jest (Javascript) in this example. Please conduct your own research to identify which library suits your team’s preferences and requirements. :)

Set up your environment

Here are the initial steps to set up your first jest project:

Create your first function

For simplicity of our discussion here, we came up with getAddressFromPostalCode(userPostal) which takes in postal code from the users. It will validate the postal code (based on a regex string) and then perform the necessary actions depending on the validity of the values entered.

Since the getAddressFromPostalCode(userPostal) is a pseudocode function, we won’t be showing the tests in our discussion. We will now create our first test for validatePostal(userPostal).

Create your first test

The basics of creating a test (and not just a unit test) lies in 3 simple steps:

  • Arrange: Set up your test data, mocks, fakes e.t.c
  • Act: Call your method under tests
  • Assert: Check that the expected results are returned

Once you understand the basics, there are some additional considerations when creating these test files/folders. If you’re using Javascript (like us), you may name your file as <actual-code-filename>.test.js. Alternatively, if you come from a more Ruby-based background, you may name your file as <actual-code-filename>.spec.js.

In the example shown above, the file is named as searchPostalCode.js. Thus, we’ll name the test file searchPostalCode.test.js and place it within the __tests__ folder.

Another consideration is that you may want to put your tests in the same place as your source code. There are pros and cons to each approach.

Like any working agreement, it is important to discuss this with your team and consider the prevailing conventions in your tech stack choice.

Run your first test

To run all your tests, simply run jest or npm test if you have added the command to your package.json script.

Congratulations! You’re finally able to start your journey towards testing modular pieces of code.

There will be a natural eagerness to want to increase your test coverage to 100%. However, the benefits reaped will start to plateau off and become negligible as compared to the effort required. It is more crucial that you use testing to design your code and refactor.

Starting on a unit testing journey? Do share with us in the comments below because #sharingIsCaring!

- Merlin 💛

Sources:

--

--