Quick Links

Unit Testing is the process of writing and automatically running tests to ensure that the functions you code work as expected. While it might seem like more work, it's really about taking preventative measures to squash bugs before they arise.

What Is a Unit Test?

A "Unit" is the smallest possible software component in your app (i.e, functions, classes, or components). Individual unit tests make sure that the core component of your app is behaving as expected, and that a future commit to one area of your code doesn't break code in another. If it does, you likely have a bug in either your new or old code (or in a poorly written/outdated test).

The goal of unit tests is obvious---reduce bugs, especially bugs that arise from integration. A developer might think everything is fine locally and commit their code, only to find out that another commit has broken the app. Unit testing helps catch some of these defects before they become issues, and when combined with automated continuous integration pipelines, can make sure that the daily build is always working properly.

Unit testing isn't limited to small bits of code; You can also unit test larger components that make use of multiple other functions, that may themselves be unit tested. This helps you track down errors more effectively---is the error in the methods of the large component object, or in one of the other components it makes use of?

While unit tests are important, they also aren't the only testing you should be doing. Running End-to-End UI testing and manual human review will catch plenty of logic bugs that unit tests may miss when every unit is operating as expected.

Unit Testing Leads to Cleaner Codebases

One of the main problems with legacy codebases is dinosaur code---code so old that it's basically a black box, you might have no idea how it works, but somehow it does work, and you don't want to refactor it due to fears it might break everything.

In a way, when you write unit tests, you're writing documentation for it.  You might not have to write a whole manual, but you'll always be defining two things: what to give the function, and what it returns, similarly to how you'd define an API schema. With these two bits of information, it's clear what the function does, and how to integrate it into your app. Obviously, unit testing doesn't solve existing legacy codebases, but it does prevent you from writing this type of dinosaur code in the first place.

Often, you'll be able to write your tests before the actual function you're testing. If you know what your function needs to do, writing the test first forces you to think about the end result of your code, and what it is responsible for.

If you like this effect of unit testing, you might also be interested in TypeScript---a compiled superset of JavaScript that makes it strongly typed. You'll still want to write unit tests, but knowing what types a function gives and takes while you're coding is a very useful feature.

How to Run Unit Tests

There are many different unit testing frameworks, and the one you ultimately use will depend on the language you're testing. To showcase how they work though, we'll be using Jest, a JavaScript testing framework that is the default for new React applications.

A Unit Test usually consists of three stages:

  • Arrange, where data is prepared for the unit to test. If you need to fetch data, build a complex object, or just set up some stuff, you'll do this first.
  • Act, where the unit is called, and the response is logged.
  • Assert, where the bulk of the testing happens. This is where you write Boolean operations based on the

If any of the assertions fail, the unit has failed the test, and you'll get a detailed log and stack trace of what went wrong, what you expected, and what was actually returned.

Jest has a bunch of different matchers, which allow you to perform quick and simple assertions. For example, say you have the following  function, which just adds two numbers:

function doSomeMath(a, b) {
    

return a + b;

}

You can test this function with the following statement:

test('Expect math to work', () => {
    

  expect(doSomeMath(1, 1)).toBe(2);

});

Usually, this is saved alongside the function under functionName.test.js. Jest will automatically look for these files when running tests.

The .toBe() function is the matcher, in this case checking for basic equality. There are many others, like .toBeEqual(), which checks for object equality, and .toContain(), which checks array contents. You can read through Jest's docs for a full list of their supported matchers.