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.
RELATED: What Is TypeScript, and Should You Use It Instead of Vanilla JS?
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:
You can test this function with the following statement:
Usually, this is saved alongside the function under functionName.test.js. Jest will automatically look for these files when running tests.
RELATED: How to Get Started with Jest for JavaScript Unit Testing
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.