Unit Testing using Mocha, Sinon, and Chai in NodeJS
Table of contents
We use Mocha as the test framework, Chai for assertions, and Sinon.js for creating Doubles. Here's the code for installation:
$ npm install sinon mocha chai --save-dev
--save-dev
is used because these modules are only needed during development. We don't need these modules during production.
The Script
The main thing to do when creating a script is to determine which function to test, along with what will be tested in that function.
For example, if we want to do unit testing on the following function:
/*number-lib.js*/
function addInteger(x, y) {
if (Number.isInteger(x) && Number.isInteger(y)) {
return x + y
}
else {
return 'Number is not integer'
}
return
}
module.exports = {
addInteger
}
The function above is a function to add integers x and y. If they are not integers, it will return a string saying that the input is not an integer.
First, we will determine our test scope as follows:
// number-list-test.spec.js
const numberLib = require('./number-lib.js')
describe('Add integer', () => {
it('should return 23', () => {
// Arrange
...
// Act
...
// Assert
...
})
it('should return "Number is not integer", because input is not integer', () => {
// Arrange
...
// Act
...
// Assert
})
})
describe()
and it()
are functions of Mocha, where we will write our test script. In the describe()
function, we determine our test scope. Inside each it()
function, we determine the cases that we will perform.
In the case above, our scope is to test the addInteger()
function with 2 agreed-upon cases, namely the function must return 23, and the function must return "Number is not integer"
because the input we will give is not an integer.
In the it()
function, there is an Arrange, Act, and Assert section. Here is an explanation:
- Arrange
In the Arrange section, it usually contains input initialization and doubles initialization which aims to produce the desired output.
- Act
In the Act section, we execute the function that we want to test using the input that we have initialized in the Arrange section.
- Assert
In the Assert section, we check whether the result produced by the function in the Act section matches the expected output that we have initialized in the Arrange section.
Now we will add the code snippet above to make it complete.
// number-list-test.spec.js
const numberLib = require('./number-lib.js')
const expect = require('chai').expect
describe('Add integer', () => {
it('should return 23', () => {
// Arrange
let x = 11
let y = 12
// Act
let result = numberLib.addInteger(x, y)
// Assert
expect(result).to.equal(23)
})
it('should return "Number is not integer", because input is not integer', () => {
// Arrange
let x = 1.1
let y = 1.2
// Act
let result = numberLib.addInteger(x, y)
// Assert
expect(result).to.equal('Number is not integer')
})
})
Doubles
For example, let's update the function we want to test like this:
function addNumber(x, y) {
if(isInteger(x) && isInteger(y))) {
return x + y
}
else if(isDecimal(x) && isDecimal(y))) {
return Math.round(x) + Math.round(y)
}
else {
return 'The input is not integer nor decimal value'
}
}
function isInteger(x) {
...
}
function isDecimal(x) {
...
}
modules.export = {
addInteger
}
Correct. There is a function inside a function. The concept of Unit Testing is that we only test the function that we want to test. If the function calls another function, then we ignore the other function by making it a Double.
In our example, we will not care about the isInteger()
and isDecimal()
functions. Both of these functions will be made into doubles with the help of sinon.
In that case, it was agreed that there are 3 cases to be tested. Here are the tests:
const sinon = require('sinon')
const expect = require('chai').expect
const numberLib = require('./number-lib')
describe('Add Number', () => {
it('should return 40', () => {
})
it('the input is decimal, should return 15', () => {
})
it('should return "The input is not integer nor decimal value"', () => {
})
})
We have determined the cases, now let's add the test script:
const sinon = require('sinon')
const expect = require('chai').expect
const numberLib = require('./number-lib')
describe('Add Number', () => {
it('should return 40', () => {
//Create new stub for isInteger that always return true
let isInteger = sinon.stub(numberLib, 'isInteger').returns(true)
let x = 20
let y = 20
let result = numberLib(x, y)
expect(result).to.equal(40)
isInteger.restore()
})
it('the input is decimal, should return 15', () => {
//Create new stub for isInteger that always return false
//stub for isDecimal always return true
let isInteger = sinon.stub(numberLib, 'isInteger').return(false)
let isDecimal = sinon.stub(numberLib, 'isDecimal').returns(true)
let x = 7.4
let y = 7.6
let result = numberLib(x, y)
expect(result).to.equal(15)
isInteger.restore()
isDecimal.restore()
})
it('should return "The input is not integer nor decimal value"', () => {
//Create new stub for isInteger that always return false
//stub for isDecimal always return false
let isInteger = sinon.stub(numberLib, 'isInteger').return(false)
let isDecimal = sinon.stub(numberLib, 'isDecimal').returns(false)
let x = 'anything'
let y = 'anything'
let result = numberLib(x, y)
expect(result).to.equal('The input is not integer nor decimal value')
isInteger.restore()
isDecimal.restore()
})
})
At the beginning of each case, we always initialize the stub for the isInteger()
and isDecimal()
functions, by changing its return value according to the needs of the test case. When numberLib()
is called, if a stub function we made before is encountered during execution, that function will be changed to a stub.
At the end of each case, we must restore() the stubs we made so that the functions that became stubs can run their own content again. If we don't do a restore()
, there is a possibility that the next test will fail because these functions are still stubs.
The above test script can still be improved with the following Mocha functions:
...
const numberLib = require('./number-lib')
describe('Add number', () => {
let isInteger
let isDecimal
beforeEach(() => {
isInteger = sinon.stub(numberLib, 'isInteger')
isDecimal = sinon.stub(numberLib, 'isDecimal')
})
afterEach(() => {
isInteger.restore()
isDecimal.restore()
})
it('...', () => {
isInteger.returns(true)
...
})
it('...', () => {
...
})
it('...', () => {
...
})
})
beforeEach()
is always called before each test case is executed, while afterEach()
is always called after each test case is executed. With the help of beforeEach()
and afterEach()
we can avoid repetitive initialization code and make our test script more readable.
Anonymous function cases
Sometimes we encounter something like this:
//my-route.js
router.get('/', (req, res) => {
//function that we want to test
...
...
})
router.get('/user', (req, res) => {
//other function
...
...
})
module.exports = router
The code above is one of the examples to create a router on express.js
.
How do we test that function, when that function can't be called from another function or so it is called Anonymous Function? We can test it with Sinon's help.
First, create the test script:
const route = ('./my-route')
describe('Route test', () => {
it('should ...', () =>{
})
})
We can see from the function, router
call get
function that has 2 parameters. First parameter is the endpoint of the route, and the second is the function that we want to test. So we need to create spy on get
function.
const route = require(./my-route)
const sinon = require('sinon')
describe('Route test', () => {
it('should ...', () => {
//create spy on get
let get = sinon.spy(route, 'get')
// Arrange input
let req = {}
let res = {}
//call the function
route.router()
..
})
}
When route.router()
called, all function (router.get('/')
dan router.get('/user')
) will be called. Because we only want to test get('/')
, the test code become like this:
const route = require(./my-route)
const sinon = require('sinon')
describe('Route test', () => {
it('should ...', () => {
let get = sinon.spy(route, 'get')
let req = {}
let res = {}
route.router()
//Calling the `first` get and then trigger anonymous function on the parameter no 1
get.firstCall.callArgWith(1, req, res)
expect(...)
get.restore()
})
}
get.firstCall
will get only the function that is called first.
get.firstCall.callArgwith(1, req, res)
will be triggering the function on second parameter (Zero based). And then, we succesfully call the anonymous function, asserting could be done.
Running
To run the test script, we need some modifications on package.json
// package.json
"scripts": {
"test": "mocha test/**/*.spec.js"
}
The goal is to running mocha
command on all files in all folders that having .spec.js
extension. So we need to name our test function with .spec.js
Run the script:
npm test
We can see the results in terminal:
> mocha test/**/*.spec.js
Add Integer
✓ should return 23
✓ should return "Number is not integer", because input is not integer
2 passing (1s)
Reporting
We need to module for this. Mocha-junit-reporter for report unit-test, and nyc for code coverage
$ npm install mocha-junit-reporter nyc --save-dev
Modify package.json
on scripts
:
"scripts": {
"report": "mocha test/**/*.spec.js --reporter mocha-junit-reporter",
"coverage": "nyc --reporter=lcov --reporter=text-lcov npm test"
},
Run it:
$ npm run report
$ npm run coverage
We will get .xml
that we can show on any other application or tool like Jenkins
References
https://martinfowler.com/articles/mocksArentStubs.html
https://www.sitepoint.com/sinon-tutorial-javascript-testing-mocks-spies-stubs/
Subscribe to my newsletter
Read articles from Fandy Aditya Wirana directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Fandy Aditya Wirana
Fandy Aditya Wirana
I am always eager to experiment with new technology and share my experiences and insights gained through trial and error. Currently working as Backend Engineer.