Unit Testing With JS Promise
Learn about executing unit testing for JavaScript with JS Promise, which can help you work with asynchronous operations more smoothly.
Join the DZone community and get the full member experience.
Join For FreeWhat Is Promise?
Promise
is an Object introduced in ECMA Script 2015 as part of the 6th Edition. It represents the future completion of success/failure operations. It mainly helps to work smoothly with asynchronous operations. The Promise API mainly follows the Promises/A+
Specification.
Why We Need Promise
For instance, you have submitted the email form/search form and are waiting for the response. In that case, you will not be sure when the server will respond and it's a bit hard to predict the time and get the actual message after that particular execution time for the response. Promise can help fix this issue.
Simple Promise
Take a look at the below snippet, which uses the Promise API to resolve the success message after two seconds. You can increase the duration as well, but the console triggers whenever it gets the message. In this example, we used the setTimeout
function to return after two seconds, like a server response.
(function simple () {
let serviceResponse = new Promise((resolve, reject) => {
setTimeout(() => resolve('Success'), 2000);
});
serviceResponse.then((msg) => {
console.log(`Hello Message is ${msg}`);
}, () => {
console.log('oops! its rejected');
});
}());
Tools
MochaJS
Mocha is a JavaScript test framework running on Node.JS, featuring browser support, asynchronous testing, test coverage reports, and use of any assertion library. It can be used with libraries like should.js, expect.js, and chai.js.
ChaiJS
Chai is a BDD/TDD assertion library for Node and the browser that can be delightfully paired with any JavaScript testing framework.
SinonJS
SinonJS is a standalone library for test spies, stubs, and mocks for JavaScript, which works with any unit testing framework.
Installation
Use one of the commands below to install Mocha, Chai, and Sinon Module. These must be installed as Dev Dependencies modules, because it’s not required when the application running in the server, but during the build.
$ npm i -D mocha chai sinon
$ npm install mocha chai sinon --save-dev
Code Coverage
If you are very much interested in how much code is covered during unit testing, try Istanbul, which makes test coverage simple with various report formats.
$ npm i -D nyc
Simple Unit Testing
Let's have a look at the below code. It has a very simple function called basic
and returns the promise data after 1.6 seconds (assume that it's retrieved from the database after 1.6 seconds).
function basic (result) {
if (!result) {
return Promise.reject('Failure');
}
return new Promise((resolve, reject) => {
// Assume that this below data is retrieved from Database which takes 1.6 seconds.
setTimeout(() => resolve(result), 1600);
});
}
module.exports = {
basic,
};
Unit Testing Basics
As explained in the previous step, we are adding Mocha and Chai for this basic Promise code. In order to write basic unit testing in Mocha, understanding the below two methods is a must.
describe
- This function helps to groups selected test cases, so its easy to refer to and maintain.it
- This function helps to write your use case and test your library/module and expect the result.
Mocha Hooks
There are a few more functions which are considered Hooks. Below are the four important hooks that can be placed inside the describe
function before any it
function.
before
- runs before all tests in this block.beforeEach
- runs before each test in this block.afterEach
- runs after each test in this block.after
- runs after all tests in this block.
Unit Testing Example
In this unit testing example, we have the main group- Basic Testing- and three subgroups- Basic, Error Case, and Success Case; each subgroup has one test case. Scroll through the code below to get a basic understanding.
const mocha = require('mocha');
const expect = require('chai').expect;
const basicFile = require('./basic');
describe('Basic Testing', () => {
const noop = () => {};
describe('Basic', () => {
it('should have method `basic`', () => {
expect(basicFile).to.be.an('object');
expect(typeof basicFile.basic).to.equal('function');
});
// You can add more basic Test Cases.
});
describe('Error Case', () => {
it('should return Error message for empty param', () => {
return basicFile.basic().then(noop, (errorMessage) => {
expect('Failure').to.equal(errorMessage);
});
});
// You can add more Error Cases.
});
describe('Success Case', () => {
it('should return param with Hello for valid param', () => {
return basicFile.basic('Success').then((successMessage) => {
expect('Success').to.equal(successMessage);
}, noop);
});
// You can add more Success Cases.
});
});
If it is still not clear, then have a look at the below explanation of each test case.
Basic - We are testing whether basic is a function.
Error Case - basic should return "Failure" when there is no parameter passed to the function.
Success Case - basic should return a given parameter as a result of a success scenario.
If you noticed the Success Case and Error Case scenario, we have used a return keyword for the Promise method so that it can wait and complete the operation. It's suggested to use return
instead of done()
for this type of test case.
Let's see a different example that includes an external file, the database library.
Unit Testing With Dependent Files
In real time, long files used to be split into a number of small files as models, libs, and services. Here, we will see a service file, which is included in the db helper file, where we get the db data.
const db = require('./db');
function getResultData(name) {
let payload;
if (name) {
payload = {
prop1: `value1-${name}`,
prop2: `value2-${name}`,
};
}
return new Promise((resolve, reject) => {
db.getData(payload).then((data) => {
if (data.error) {
reject(data);
}
resolve(data);
});
});
}
module.exports = {
getResultData,
};
DB Helper File - this is the file used above:
function getData(payload) {
return new Promise((resolve, reject) => {
if(!payload) {
reject({
error: true,
});
}
setTimeout(() => {
resolve({
name1: 'value1',
name2: 'value2',
});
}, 5000);
});
}
module.exports = {
getData,
};
This unit test included Mocha, Chai, and Sinon libraries, and files like Service and DB Helper.
Here, you will notice that before
, beforeEach
, and after
hooks were added for method request params and response with Sinon Stubs. It actually helps to proxy the calls of the DB library.
Why We Need Sinon
SinonJS has many good features. Below are a few methods we used for Stubs.
const mocha = require('mocha');
const expect = require('chai').expect;
const sinon = require('sinon');
const db = require('./db');
const service = require('./service');
describe('service.js', () => {
let noop, label, payload, response, dbStub;
before(() => {
noop = () => {};
label = 'emp';
payload;
response;
dbStub;
});
beforeEach(() => {
payload = {
prop1: `value1-${label}`,
prop2: `value2-${label}`,
};
response = {
name1: 'value1',
name2: 'value2',
};
dbStub = sinon.stub(db, 'getData').callsFake(() => {
return Promise.resolve(response);
});
});
afterEach(() => {
dbStub.restore();
});
it('should return error response', () => {
return service.getResultData().then(noop, (data) => {
expect(data).to.deep.equal({
error: true,
});
});
});
it('should return success response', () => {
return service.getResultData(label).then((data) => {
expect(data).to.deep.equal(response);
});
});
});
Unit Testing With Async.js
Async is a utility module which provides straightforward, powerful functions for working with asynchronous JavaScript. Although originally designed for use with Node.js and installable via npm install --save async
, it can also be used directly in the browser.
In this below example, We use async.parallel
where we need to trigger the DB Service Method at the same time.
const async = require('async');
const db = require('./db');
function myAsyncParallel (callback) {
async.parallel({
first: db.userData.bind(null, 'first arg'),
second: db.activityData.bind(null, 'second arg')
}, callback);
}
module.exports = {
myAsyncParallel,
};
Similar to the previous example, we added Mocha, Chai, and Sinon and added only one example for a success scenario.
We are mainly bypassing the DB Methods db.userData and db.activityData to return the custom data using Sinon Stub.
In this test case, we are checking that the expected result is equal to the method response.
const mocha = require('mocha');
const expect = require('chai').expect;
const sinon = require('sinon');
const db = require('./db');
const asyncFile = require('./parallel');
describe('Async Parallel.js', () => {
let params, resultValues, userDataStub, activityDataStub, activityDataSpy;
before(() => {
params = ['first arg', 'second arg'];
resultValues = ['First Result', 'Second Result'];
});
beforeEach(() => {
userDataStub = sinon.stub(db, 'userData').callsFake((arg, callback) => {
callback(null, {
arg,
result: resultValues[0]
});
});
activityDataStub = sinon.stub(db, 'activityData').callsFake((arg, callback) => {
callback(null, {
arg,
result: resultValues[1]
});
});
});
it('should return the response', () => {
return asyncFile.myAsyncParallel(function(error, results) {
expect(results).to.deep.equal({
first: {
arg: params[0],
result: resultValues[0]
},
second: {
arg: params[1],
result: resultValues[1]
}
});
});
});
});
Stay tuned for the next article. Happy coding!
Published at DZone with permission of Venkatraman Ramamoorthy. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments