Unit Tests in Javascript in 5 Mins
Setting up unit tests and code coverage for your javascript app
Building an app involves various steps, right from gathering the requirements to finally deploying the application in production. As much as we want to expedite these steps, skipping a few steps here and there to save time, is not really recommended. There are many such steps in the stages of development that one might either love or hate and usually ‘unit testing is not much loved’.
Now few questions might arise like what are unit tests and why are we even talking about them? What is the importance of writing unit tests? These are valid questions and I’ve tried answering the same in this article. I’m sure by the end of this article you’ll definitely have a better understanding of it.
What are unit tests? What is the importance of writing unit tests?
As the name suggests, unit testing is done to test a certain unit or component of your software application. The main purpose of unit testing is to test if your code is working as expected and prevent any unwanted bugs. Properly written unit tests also ensure that your code is maintainable and isn’t breaking any existing functionalities while adding new ones.
For example: Consider a scenario where you got a task to update a condition in some existing function, being a simple change you comfortably went ahead and did it. Everything worked fine till the day when there is some issue in production because of the change of the logic, these types of issues can be catastrophic for an app. Perfectly written unit tests would have easily prevented the situation from occurring by predetermining the effect of your code change.
The keyword unit testing may lead us to think this is done in the testing phase but that's not true, unit tests should be as much part of your code(application) as any other part of your code.
Further in this article, we will take a look at how can you easily set up unit tests for your node.js app and also generate the code coverage for the app.
Sample Node.js App
For this example, we will be using a sample node.js app which is a simple calculator and consists of the API to perform operations like add, subtract, multiply and divide. Along with the above operations, I have also included an example depicting the DB operations, more on this will be discussed further in this article.
- calculator.js: This file contains different functions for different calculator operations like add, subtract, multiply and divide.
- index.js: This includes the APIs to perform different operations of the calculator.
- DB.js: This includes the dummy functions for DB connection and fetching values from DB.
SampleNodeApp Unit Tests
Now we are ready with the sample node.js app, we can test our ‘SampleNodeApp’ in the postman by running and calling different APIs exposed by the app.
But is testing APIs manually sufficient? Maybe: Yes for this small app but as the size of the app keeps increasing, it becomes increasingly difficult to test the app with different sets of inputs and ensure any new changes are not breaking the existing code. That’s where unit tests come to the rescue!
Libraries used
For unit testing, this app we’ll be using the following libraries :
- Mocha: This is the simple javascript test library to Node.js. Mocha tests run serially, allowing for flexible and accurate reporting while mapping uncaught exceptions to the correct test cases. For more on the Mocha visit.
- Chai: This is a unit test assertion framework that can be paired with the different javascript frameworks. Chai also provides different plugins for testing different types of methods. for testing API we will use the chai-http plugin. For more on Chai visit.
- Sinon: This is a popular javascript unit test mocking framework. Sinon helps to mock the behavior of different functions making some predefined assumptions while testing. For example, we may not want to call the DB while unit testing, in this case, Sinon will help you to write a mock for DB functions. For more on Sinon visit.
- NYC/Istanbul: After writing unit tests we might want to check if we have written tests covering each branch and case. For this purpose, we can generate the code coverage report. NYC helps to generate the code coverage for our unit tests. Whereas, Istanbul helps to generate the detailed report in the form of HTML and other supported forms.
For your reference after installing all the libraries package.json will look like this:
Now we know about different unit test libraries, we can start writing unit tests for our application
SampleAppTests
All the tests written using a mocha framework are by default written under the test directory under the main directory. To begin with, we’ll create a new file test/SampleAppTests.js.
- Imports: This is the simplest part where like any other node.js file we need to import the required dependencies and the modules required in this file
- Chai Plugins: As discussed in the library section of this article chai framework allows different existing plugins and has the facility to create one of your own. For our current use case, we will use two plugins chai-http and should.
- describe/it: describe is used to make/ divide tests into human-readable sections. We can define as many hierarchy levels using describe as we want. In the above example, we are using divide the tests into different categories. it is used to define and run tests in mocha. When we run the command for tests it is the actual function that will run the tests for us.
- Negative tests: While we are busy writing tests and covering all the positive cases for functionalities inside our application we can easily forget to cover the negative cases for our tests. Not covering the negative cases will result in lesser coverage and may result in missing important issues. So negative unit tests are as important as writing positive tests.
- Mocking: Understanding Mocks is really important and even sometimes necessary when you wanted to test functions that are dealing with DB and some other data sources. In the above example, we are mocking the function interacting with the DB. For mocking functions, we need to set some assumptions that this function will always return the mocked result. Also, note that any functions mocked will not be considered as covered and bring down your overall coverage so choose wisely which function needs to be mocked. As a thumb rule, we should avoid mocking functions that are internal to the application and do not interact with DB or external sources.
After adding all the files, the folder structure will look like the following:
Running tests
For running the test, if we are using the provided package.json file, in that we have already added the scripts. After setting the scripts we just need to run the following command.
npm test
After running the above command we can we can see output and code coverage in the terminal.
Did you see the output….? Congrats!!!! It's almost done, but it is still not perfect.
Bonus Tip
If you notice DB.js has code coverage in red. This is also bringing overall code coverage down. This is due to the mock function which we created to write test cases. Then what is the solution?
Often we might want to exclude/ include some files or even some parts of code not to be considered in code coverage. NYC provides functionality to easily do the same with few lines for configuration. For this, we just need to add .nycrc in the parent directory of our project. Along with the include/exclude .nycrc also provides a few other functionalities.
Now let’s see the output after adding the above config file. As you can see, after removing the unwanted files, the coverage increased moved to 100%.
If you are able to replicate the above code, run the tests and generate the code coverage, Excellent Job!!!
For those who were not able to run and replicate the same, No Worries!! I have added code for all the important files in the next section.
//calculator.js
function calculator(operation, num1, num2) {
let res;
switch (operation) {
case "add":
res = add(num1, num2);
break;
case "subract":
res = subract(num1, num2);
break;
case "multiply":
res = multiply(num1, num2);
break;
case "divide":
res = divide(num1, num2);
break;
}
console.log(res);
return new String(res);
}
function add(num1, num2) {
return num1 + num2;
}
function subract(num1, num2) {
return num1 - num2;
}
function multiply(num1, num2) {
return num1 * num2;
}
function divide(num1, num2) {
return num1 / num2;
}
module.exports = { calculator };
//index.js
const express = require("express");
const { calculator } = require("./calculator");
const DB = require("./DB");
const app = express();
const port = 3000;
const db=new DB()
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
app.get("/calculator/:operation", validateOperation(),(req, res) => {
let num1 = req.query.num1;
let num2 = req.query.num2;
let operation = req.params.operation;
if (!operation || !num1 || !num2) return "invalid values";
return res.send(calculator(operation, Number(num1), Number(num2)));
});
app.get("/DBcalculator", (req, res) => {
let val=db.calculateFromDb()
return val;
})
//middleware for validation
function validateOperation() {
const errorMessage =
"Invalid operations: operations allowed add, subract, multiply, divide";
const validOperation = ["add", "subract", "multiply", "divide"];
return (req, res, next) => {
let operation = req.params.operation;
if (!operation || !validOperation.includes(operation)) {
return res.status(400).json({ Message: errorMessage });
}
next();
};
}
module.exports = app
//DB.js
const sql = require("mysql");
const credentials = {
host: "localhost",
user: "me",
password: "secret",
database: "my_db",
};
module.exports = class DB {
//Get the DB Connection
static getConnection() {
return sql.createConnection(credentials);
}
//DB operation
calculateFromDb() {
const connection = DB.getConnection();
connection.query(
"SELECT 1 + 1 AS solution",
function (error, results, fields) {
if (error) throw error;
console.log("The solution is: ", results[0].solution);
}
);
return "Value from DB";
}
};
//package.json
{
"name": "samplenodeapp",
"version": "1.0.0",
"description": "A sample for testing unit tests",
"main": "index.js",
"scripts": {
"test": "nyc --reporter=clover --reporter=text mocha --exit --timeout 50000"
},
"author": "Jaideep Pahwa",
"license": "ISC",
"devDependencies": {
"chai": "^4.3.7",
"chai-http": "^4.3.0",
"express": "^4.18.2",
"istanbul": "^0.4.5",
"mocha": "^10.1.0",
"nyc": "^15.1.0",
"sinon": "^14.0.1"
},
"dependencies": {
"mysql": "^2.18.1"
}
}
//test/SampleAppTests.js
const { should } = require("chai");
let chai = require("chai");
// required for testing the api
let chaiHttp = require("chai-http");
//required for mocking methods
const sinon = require("sinon");
const DB = require("../DB.js");
chai.use(chaiHttp);
chai.use(should);
const server = require("../index.js");
describe("Sample app test cases", () => {
describe("Testing calculator API", () => {
let num1 = 2,
num2 = 1;
//positive test cases
it("test caclulator for valid id inputs", (done) => {
chai
.request(server)
.get(`/calculator/add?num1=${num1}&num2=${num2}`)
.end((err, res) => {
res.should.have.status(200);
res.body.should.be.a("string");
res.body.should.be.eql("3");
});
done();
});
it("test caclulator for valid id inputs", (done) => {
chai
.request(server)
.get(`/calculator/subract?num1=${num1}&num2=${num2}`)
.end((err, res) => {
res.should.have.status(200);
res.body.should.be.a("string");
res.body.should.be.eql("1");
});
done();
});
it("test caclulator for valid id inputs", (done) => {
chai
.request(server)
.get(`/calculator/multiply?num1=${num1}&num2=${num2}`)
.end((err, res) => {
res.should.have.status(200);
res.body.should.be.a("string");
res.body.should.be.eql("2");
});
done();
});
it("test caclulator for valid id inputs", (done) => {
chai
.request(server)
.get(`/calculator/divide?num1=${num1}&num2=${num2}`)
.end((err, res) => {
res.should.have.status(200);
res.body.should.be.a("string");
res.body.should.be.eql("2");
});
done();
});
});
describe("Testing calculator negative cases API", () => {
let num1 = 2,
num2 = 1;
//negative test cases
it("test caclulator for invalid id inputs: invalid operation", (done) => {
chai
.request(server)
.get(`/calculator/addd?num1=${num1}&num2=${num2}`)
.end((err, res) => {
res.should.have.status(400);
res.body.should.be.a("object");
});
done();
});
});
describe("Testing calculator API", () => {
//Testing DB methods by mocking
it("test the db methods by mockning functions", (done) => {
sinon.stub(DB.prototype, "calculateFromDb").returns("Value from DB");
chai
.request(server)
.get(`/DBcalculator`)
.end((err, res) => {
res.should.have.status(200);
res.body.should.be.a("string");
});
done();
});
});
});
//.nycrc
{
"include": [
"*.js"
],
"exclude": [
"DB.js"
],
"check-coverage": true,
"lines": 65
}
Summary
In this article, we started with the introduction to the unit tests and got to know their importance. Then took a look into a sample node.js app and how we can create the unit tests for the node.js app using the different javascript frameworks. As a bonus tip, we also looked into how we can increase our code coverage by removing unwanted code and files.
If you loved my work please like and share this article( it’s free :)). Also, do follow me for more articles like these.
Also, check out my other articles: