Testing code is the first step to making it secure. One of the best ways to do this is to use unit tests, ensuring that each of the smaller functions within an app behave as they should — especially when the app receives edge-case or invalid inputs, or inputs that are potentially harmful.
Why should we unit test?
When it comes to unit testing, there are many different approaches. Some primary purposes of unit tests are:
- Verifying functionality : Unit tests ensure that code does the right thing and doesn’t do anything it’s not supposed to — which is where most bugs occur.
- Preventing code regression : When we find a bug, adding a unit test to check the scenario keeps code changes from re-introducing the bug in the future.
- Documenting code : With the right unit tests, a complete set of tests and results provides the specifications for how an app is supposed to work.
- Securing your app : Unit tests can check for exploitable vulnerabilities (such as those that enable malicious SQL injection).
Scoping and writing unit tests
Using a unit test framework enables us to quickly write and automate our tests and integrate them into our development and deployment processes. These frameworks often support testing in both front and back-end JavaScript code.
Below are some general guidelines to help you write performance unit tests and testable code.
Keep unit tests short and simple
Don’t overload your unit tests. The test should only be a couple of lines of code that check short, functional chunks of your application.
Consider both positive and negative test cases
Writing a test to confirm that a function executes properly is helpful. However, it’s far more effective to write a broader set of tests that check whether a function fails properly if misused or in edge cases. These negative tests can be even more valuable because they help anticipate unexpected scenarios, such as when a function should throw an exception or how it should handle receiving malformed data.
Break apart long and complex functions
Large functions that contain a lot of logic are difficult to test; including too many operations prevents testing each variable effectively. If a function is too complex, split it into smaller functions for individual testing.
Avoid network and database connections
Unit tests should be quick and lightweight, but functions that make network calls or connect to other apps or processes take a long time to execute. This makes it challenging to run many operations simultaneously and can create more fragile code. You can use mocking in unit tests to enable simulated network or database calls — this lets you test the rest of the function. You can include the real network and database connections during a different test process called integration testing.
How to code unit tests
Now that we’ve reviewed some unit testing best practices, you’re ready to write your first unit test in JavaScript.
This tutorial uses the Mocha Framework — one of the most popular for unit testing. Each test framework is slightly different, but they’re similar enough that learning the basic concepts will enable you to switch between them easily.
To follow this demonstration, ensure you have Node.js installed on your computer.
Creating a new project
Start by opening a terminal window or command prompt to a new project folder. Then, create a new Node.js project inside it by typing npm init -y
.
This creates a package.json
file inside the folder, enabling you to install Mocha as a development dependency using npm install -D mocha
.
Next, open the package.json
file in your code editor and replace the placeholder test script with mocha
:
"scripts": {
"test": "mocha"
},
Implementing a class
Next, code a simple traffic light system to unit test.
Inside your project directory, create a file called traffic.js
and add the following code for a TrafficLight
class:
class TrafficLight {
constructor() {
this.lightIndex = 0;
}
static get colors() {
return ["green", "yellow", "red"];
}
get light() {
return TrafficLight.colors[this.lightIndex];
}
next() {
this.lightIndex++;
// This is intentionally wrong!
if( this.lightIndex > TrafficLight.colors.length ) {
this.lightIndex = 0;
}
}
}
module.exports = TrafficLight;
This class consists of four parts:
-
TrafficLight.colors
: a constant property for the traffic light colors. -
lightIndex
: a variable tracking the index of the current traffic light color. -
light
: a class property that returns the current traffic light color as a string. -
next()
: a function that changes the traffic light to the next light color.
Configuring and adding our first unit test
Now it’s time to add some unit tests around the code.
Create a folder in the project directory called test
. This is where Mocha will check for unit tests by default. Then, add a file named traffic.test.js
inside the new test folder.
Next, import the TrafficLight
class at the top of the file:
const TrafficLight = require( "../traffic" );
We’ll also use the assert
module for tests, so require it in your code:
const assert = require( "assert" );
With Mocha, we can group unit tests into sets using the describe()
function. So we can set a top-level group for this class as follows:
describe( "TrafficLight", function () {
});
Then, we’ll create some unit tests to validate the traffic colors in their own sub-group, called colors
inside the TrafficLight
set:
describe( "TrafficLight", function () {
describe( "colors", function () {
});
});
For this first unit test, we can verify that colors
only has three states: green, yellow, and red. The test is defined using an it()
function inside the describe()
group, so write the test like this:
describe( "TrafficLight", function () {
describe( "colors", function () {
it( "has 3 states", function () {
const traffic = new TrafficLight();
assert.equal( 3, TrafficLight.colors.length );
});
});
});
Now let’s run the unit test and see if it passes.
Run npm test
in your terminal window, and if everything is correct, Mocha prints out the results of your unit test run.
Adding more unit tests
Our project is now ready to run unit tests, so we can add more tests to ensure our code is working properly.
First, add a unit test to the colors
group to validate that the traffic light colors are correct and in order. Here’s one way to implement this test:
it( "colors are in order", function () {
const expectedLightOrder = ["green", "yellow", "red"];
const traffic = new TrafficLight();
for( let i = 0; i < expectedLightOrder.length; i++ ) {
assert.equal( expectedLightOrder[i], TrafficLight.colors[i] );
}
});
Second, test the next()
function to see if it changes the traffic lights correctly. Create a new sub-group and add two unit tests: one to check that the lights change in the correct order, and another to check that the light loops after red
back to green
.
Write these unit tests as follows:
describe( "next()", function () {
it( "changes lights in order", function () {
const traffic = new TrafficLight();
for( let i = 0; i < TrafficLight.colors.length; i++ )
assert.equal( traffic.light, TrafficLight.colors[i] );
traffic.next();
}
});
it( "loops back to green", function () {
const traffic = new TrafficLight();
// Change the light 3x to go from green -> yellow -> red -> green
for( let i = 0; i < 3; i++ ) {
traffic.next();
}
assert.equal( traffic.light, TrafficLight.colors[0] );
});
});
Now, when we rerun the tests, we’ll see that one of the tests fails. This is because there’s a bug in the TrafficLight
class.
Fixing the bug
Open the TrafficLight
class code again and locate the comment inside the next()
function that reads // This is intentionally wrong!
From our unit tests, we know that this function isn’t correctly looping back to green
. We can see that the code currently checks for when the lightIndex
value surpasses the number of traffic light colors, but the index begins at 0
. Instead, we must loop back as soon as the value reaches the exact number of colors. Let’s update the code to reset the lightIndex
value back to 0
when it is equal to the length of the list of traffic light colors:
// This is intentionally wrong!
if( this.lightIndex === TrafficLight.colors.length ) {
this.lightIndex = 0;
}
Now all of your unit tests should pass. And the best part is that even if the TrafficLight
class becomes refactored or modified heavily, our unit tests will catch this bug before it reaches the user.
Wrapping up
Unit tests are easy to set up and an effective tool for software development. They help eliminate bugs early and prevent them from returning. This keeps projects more manageable and easy to maintain, even as they become larger and more complex — especially among bigger development teams. Automated tests like these also enable developers to refactor and optimize their code without worrying about whether the new code is behaving correctly.
Unit tests are a key part of your development process and are crucial to help you build better and more secure JavaScript apps.
Happy testing!
Unit tests are great for bug fixes — code scans help too.
Scan your code for free with Snyk.