Node.js module testing with Chai and Sinon framework
Introduction
In this article I will show you how to write unit tests for Node.js modules and its dependencies from scratch. Before that, I would like to explain, why writing unit tests is important.
Every small change pushed to repository may break our application, which may be a first step to lose money. Our application can crash or just stop processing requests or even process requests in wrong way. Just imagine what would happen, if application which manages baggage drop off at the airport stops – a lot of travellers would not be able to fly to their destination – sounds like chaos. Airport might be sued and forced to pay compensations. You might ask – why someone would make this kind of application in JavaScript? You need to remember that programming language is only a tool that fulfils business needs. To leverage this kind of threats we should invest our time for writing unit tests, integration tests and end-to-end tests.
We need to write good quality tests, not the ones which only improve code coverage report. This is very important because it might alert us, that we are trying to deploy broken code on production environment. I had a lot of situations when unit tests prevented us from deploying broken build.
First test
In order to start, we should create a directory for our project and prepare libraries we need. To do so, we need to open our terminal/cmd and create new directory. Inside our project folder let’s initialize package.json by tapping:
npm init
Let’s name out project to node-test-tutorial, then exit wizard by pressing Enter key. Now we need to install required dependencies (sinon, chai, mocha and babel). Mocha is test runner for JavaScript. To install these dependencies simply run:
npm install mocha sinon chai babel-cli babel-preset-es2015 babel-preset-stage-2 node-fetch babel-core --save-dev
This will install these modules and update package.json file. We need now to alter package.json file in order to configure our application commands to run and test. We need to change scripts key to look like below:
"scripts": {
"start": "babel-node app/app.js --presets es2015,stage-2",
"test": "mocha tests/**/**.spec.js --compilers js:babel-core/register"
},
After saving changes, each type you will enter npm start Node.js will run app/app.js file, and when we run npm test script, it will search inside test folder for JavaScript test files(which fits pattern *.spec.js). Let’s now create first test file, which will contain two test cases. Inside test folder create file example.spec.js and fill it with provided code:
const expect = require('chai').expect;
describe('First test', () => {
it('should sum correctly 2 and 3', function () {
const result = 2 + 3;
expect(result).to.equal(5);
});
it('should not sum correctly 2 and 3', () => {
const result = 2 + 3;
expect(result).to.equal(6);
});
});
After running npm test command, we will see an error message:
AssertionError: expected 5 to equal 6
Now let’s just remove second test, to make test pass.
Configuring .babelrc
Our application will be written using ES6 syntax, so there is a need to setup .babelrc file in order to run tests with ES6 JavaScript syntax. Just create this file in project folder and fill it with data:
{
"presets": ["es2015"]
}
Writing app code
Our small app will have two modules – first one will be responsible for communication with external weather api in order to get current weather. Second module will implement first module and process retrieved data. Below you can get code for these modules:
api.js
const fetch = require("node-fetch");
/*
* Gets weather info for specified city
*/
export async function getWeatherData(city){
const url = 'http://api.openweathermap.org/data/2.5/weather?appid=903cb5f008420dd10ed453c37c619d74&units=metric&q=' +
encodeURIComponent(city);
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
// console.log(error);
}
}
app.js
import {getWeatherData} from './api'
export const TEMPERATURE_FREEZING = 'Temperature is below 0, remember to take coat and scarf!';
export const TEMPERATURE_COLD = 'Temperature is below 10, remember to take coat!';
export const TEMPERATURE_OK = 'Temperature is ok today';
export const TEMPERATURE_HOT = 'Warning! Temperature is above 30 degrees, watch for sunburn!';
export const TEMPERATURE_ERROR = 'API Error. Wrong temperature specified';
export const NO_CITY = 'No city specified';
export const getWeatherInfo = async(city) => {
if (!city) {
return NO_CITY;
}
const weather = await getWeatherData(city);
return getTemperatureMessage(weather.main.temp);
};
export const getTemperatureMessage = (temperature) => {
if (temperature === undefined) {
return TEMPERATURE_ERROR;
}
switch (true) {
case (temperature < 0):
return TEMPERATURE_FREEZING;
break;
case (temperature < 10):
return TEMPERATURE_COLD;
break;
case (temperature < 30):
return TEMPERATURE_OK;
break;
case (temperature >= 30):
return TEMPERATURE_HOT;
break;
default:
return TEMPERATURE_ERROR;
}
};
async function main() {
const message = await getWeatherInfo('Krakow');
console.log(message)
}
main();
Writing the test
Firstly we need to create file app.spec.js
inside test folder, and import files that we need for testing purposes:
import * as app from '../app/app';
import * as api from '../app/api';
const expect = require('chai').expect;
const sinon = require('sinon');
Then we can test first method exported from app.js
file
describe('App.js test', () => {
describe('Check temperature messages ranges', () => {
it('Temperature not specified', () => {
const message = app.getTemperatureMessage();
expect(message).to.equal(app.TEMPERATURE_ERROR);
});
});
});
In this test, we are calling getTemperatureMessage
method without any parameter. This method should return temperature error specified in app.js
file. We can check this by using expect method from chai (assertion check). This covers only one path in this method. We should also cover other paths (depending on temperature), so our next test cases will look like:
describe('Check temperature messages ranges', () => {
it('Temperature not specified', () => {
const message = app.getTemperatureMessage();
expect(message).to.equal(app.TEMPERATURE_ERROR);
});
it('Temperature below 0', () => {
const message = app.getTemperatureMessage(-20);
expect(message).to.equal(app.TEMPERATURE_FREEZING);
});
it('Temperature in range 0-10', () => {
const message = app.getTemperatureMessage(5);
expect(message).to.equal(app.TEMPERATURE_COLD);
});
it('Temperature in range 10-30', () => {
const message = app.getTemperatureMessage(27);
expect(message).to.equal(app.TEMPERATURE_OK);
});
it('Temperature above 30 degrees', () => {
const message = app.getTemperatureMessage(100);
expect(message).to.equal(app.TEMPERATURE_HOT);
})
});
This covers all paths in this single method. Now we need to test getWeatherInfo
method which makes an external api call. We don’t want to call this api during tests, so we need to use stub from Sinon framework. It will replace stubbed method with the other one. We need to replace external method each time we run test, so it’s a good way to use beforeEach
and afterEach
methods.
describe('getWeatherInfo method ', () => {
let weatherApiCallStub;
beforeEach(() => {
const resp = {main: {temp: 10}};
weatherApiCallStub = sinon.stub(api, 'getWeatherData')
.callsFake(() => {
return Promise.resolve(resp)
});
});
afterEach(() => {
weatherApiCallStub.restore();
});
it('getWeatherInfo with city specified', async() => {
const message = await app.getWeatherInfo('Krakow');
sinon.assert.called(weatherApiCallStub);
expect(message).to.equal(app.TEMPERATURE_OK);
});
};
So, before each test we are replacing getWeatherData
method (which is responsible for asynchronous api request) with Sinon stub. This stub will return Promise with prepared data. After each test we will restore stubbed method to original one – to prevent any issues in other tests by calling restore()
metod. If you want only to check if method was called, you can use spy method from Sinon framework.
You might notice, that this test is used with async
/await
keywords – it’s possible to test asynchronous functions in nice clear way without callback hell thanks to babel. In this test we are checking if external api was called and we also check return message (which is not necessary, because we tested messages in previous test).
We are only missing one more test – the second path of getWeatherInfo
metod – when city is not specified:
it('getWeatherInfo without city specified', async() => {
const message = await app.getWeatherInfo();
expect(message).to.equal(app.NO_CITY);
expect(weatherApiCallStub.called).to.equal(false);
});
Summary
After reading this short article you should know how to write unit tests for Node.js modules from scratch using Mocha framework with Chai assertion framework and Sinon for spying and stubbing methods. Source code of this small application is available at Github: https://github.com/GrzegorzWziatek/es6-test-node-chai-sinon
Testing might save you a lot of troubles in the future – so every time someone says – “there is no time for unit tests” you should be aware that your application might not cover all corner cases and further development might be difficult. With unit tests you can do big changes in your code without worrying, that something might be broken – unit tests will show you what you have to fix. Good unit tests can also help with introducing new employee to project. New employee can learn how application works by reading test code. There is one quote in the Internet, that I remember till today: “Imperfect tests, run frequently, are much better than perfect tests that are never written at all”.