NodeJS 8 from Scratch — Part 5
In previous parts, we discussed about —
- basics of nodeJS
- debugging
- asynchronous programming
- call stack and event loop
- promises
- express and deploymeny
Part 1 — https://medium.com/@anujbaranwal/nodejs-8-from-scratch-part-1-a3c1431f1e15
Part 2 — https://medium.com/@anujbaranwal/nodejs-8-from-scratch-part-2-3035f8f46b09
Part 3 — https://medium.com/@anujbaranwal/nodejs-from-scratch-part-3-20956ec252a3
Part 4 — https://medium.com/@anujbaranwal/nodejs-from-scratch-part-4-d0aadf019c79
In this part, we will discuss about —
- testing
- mocha
- assertion library — expect
- testing async code and express applications
- supertest
So, let’s get started. It is important to write unit test cases for the code that you write to make sure that the new features that are added to your application are well tested and that it doesn’t break any other features that are already existing in the application
For nodeJS applications, the testing setup is quite easy. We will be using mocha testing framework to set up our tests
Mocha
A feature-rich javascript test framework, meaning that it has all the tools needed to write tests, running on Node.js and in the browser, making asynchronous testing simple and fun.
The tests run serially, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases. The test cases can easily be organised and see how they relate to the file for which they are written.
mkdir nodejs_tests
in nodejs_tests
, run npm init
in utils/utils.js
module.exports.add = (a, b) => a + b;
To install mocha, run npm install --save-dev mocha
This is a dependency that will be useful for us while development of the application. Therefore, it is not needed to be put with the app code.
in utils/utils.js
module.exports.add = (a, b) => { return a + b;};
in utils/utils.test.js
const utils = require('./utils');it('should add two values correctly', () => { const val = utils.add(3, 5); if (val !== 8) { throw new Error(`Value is not 8. It is ${val}.`); // if this gets executed, it will make the test to fail }
});
When we run utils.test.js
with mocha, it automatically injects the it
and describe
in the file. it
represents a particular unit test case. It is a function provided by mocha. It defines a new test cases. It expects two params —
- string describing the test case
- function that tests that something work as expected
To make sure that mocha captures this file, we need to update the package.json
—
"scripts": {
"test": "mocha **/*.test.js" // recursively in all subdirectories - **
}
However, each time we have to run tests manually. We can also configure nodemon to watch for any changes and run tests automatically without us intervening
nodemon --exec "npm test"
in package.json
"scripts":{
"test": "mocha **/*.test.js",
"test-watch": "nodemon --exec \"npm test\""
}
Run npm run test-watch
to watch for any changes and to run tests continuously.
Assertion Library
It has been earlier in test cases that we were throwing error to fail a particular test case. The code will get really messy if we keep failing test cases like that for other scenarios as well. To avoid this, we can make use of assertion libraries which provides simple and elegant syntax to test behaviours and values
There are many assertion libraries available. But the one that we will be using the expect
library
npm install --save-dev expect@1.20.2
Some major changes has happened to expect in the latest version
With epxect
, the tests will look like —
in utils/utils.test.js
const utils = require('./utils');const expect = require('expect');it('should add two values correctly', () => {const val = utils.add(3, 5);expect(val).toBe(8);expect(typeof val).toBe('number');});it('should square the number correctly', () => {const val = utils.square(3);expect(val).toBe(9);expect(typeof val).toBe('number');});
However, testing arrays and objects can not be done using toBe
it('should assert array and object inclusion, equality and exclusion correctly', () => {expect([1,2,3,4]).toInclude(1);expect([1,2]).toEqual([1,2]);expect({name: 'Anuj', age: 23}).toEqual({name: 'Anuj', age: 23});expect({name: 'Anuj', age: 23}).toInclude({age: 23});expect({name: 'Anuj', age: 23}).toExclude({name: 'Ankit'});});
in utils/utils.js
module.exports.enterUserName = (user, fullName) => {const names = fullName.split(' ');user.firstName = names[0];user.lastName = names[1];return user;};
in utils/utils.test.js
it('should fill the firstName and lastName correctly', () => {const user = {location: 'Surat', age: 25};const newUser = utils.enterUserName(user, 'Anuj Kumar');expect(newUser).toInclude({firstName: 'Anuj'});expect(newUser).toInclude({lastName: 'Kumar'});});
Branch — 11_Init
Now that we have learned how to test basic assertions in nodeJS. Let’s test some asynchronous code since this is what nodeJS app is pretty much filled with.
Testing asynchronous code
in utils/utils.js
module.exports.asyncAdd = (a, b, callback) => {setTimeout(() => {callback(a + b);}, 1000);};
in utils/utils.test.js
it('should add asynchronously correctly', () => { // <- 1utils.asyncAdd(3, 5, (sum) => {expect(sum).toBe(9); // <- 2});}); // this always passes??
The reason that the test always passes is that before the callback (2) gets executed, (1) was already returned. Thus it is marked as passed. To correct this —
in utils/utils.test.js
it('should add asynchronously correctly', (done) => { // <- 1utils.asyncAdd(3, 5, (sum) => {expect(sum).toBe(9); // <- 2
done();});});
done tells mocha that it is a asynchronous operation and wait for it to get called and then only consider the test as finished. Therefore, until the (2) gets executed and done is called, it is not completed.
Testing express applications
It is important that we should be able to test express applications as this is what we will be doing mostly in the node series.
Let’s create a express server
in server/server.js
const express = require('express');const app = express();const port = process.env.PORT || 3000;app.get('/', (req, res) => {res.send('Test');})app.listen(port, () => {console.log(`Server listening on port: ${port}`);});module.exports.app = app;
To test express, we will make use of module called as supertest
npm install supertest --save-dev
in server/server.test.js
const request = require('supertest');const expect = require('expect');const app = require('./server').app;it('should respond with the correct status and response', (done) => {request(app).get('/').expect(200).expect('Test').end(done);});it('should respond with the 404 status and response as error: Page Not Found', (done) => {request(app).get('/err').expect(404).expect((res) => {expect(res.body).toInclude({error: 'Page Not Found!'}); // able to combine expect library with supertest}).end(done);});
Branch — 12_Init
Organising tests with describe
If the tests are running, you will se seeing output on the terminal —
Currently, all the tests are together. Thus it is not quite clear how the tests are related or grouped in a particular fashion. However, we can use describe
to group test cases. It will be better to read and scan through the test cases in the logs
in utils/utils.test.js
const utils = require('./utils');const expect = require('expect');describe('Utils', () => {describe('#Add', () => {it('should add two values correctly', () => {const val = utils.add(3, 5);expect(val).toBe(8);expect(typeof val).toBe('number');});});describe('#Square', () => {it('should square the number correctly', () => {const val = utils.square(3);expect(val).toBe(9);expect(typeof val).toBe('number');});});describe('Other utils', () => {it('should assert array and object inclusion, equality and exclusion correctly', () => {expect([1,2,3,4]).toInclude(1);expect([1,2]).toEqual([1,2]);expect({name: 'Anuj', age: 23}).toEqual({name: 'Anuj', age: 23});expect({name: 'Anuj', age: 23}).toInclude({age: 23});expect({name: 'Anuj', age: 23}).toExclude({name: 'Ankit'});});it('should fill the firstName and lastName correctly', () => {const user = {location: 'Surat', age: 25};const newUser = utils.enterUserName(user, 'Anuj Kumar');expect(newUser).toInclude({firstName: 'Anuj'});expect(newUser).toInclude({lastName: 'Kumar'});});});describe('Async', () => {it('should add asynchronously correctly', (done) => { // <- 1utils.asyncAdd(3, 5, (sum) => {expect(sum).toBe(8); // <- 2done();});});});});
in server/server.test.js
const request = require('supertest');const expect = require('expect');const app = require('./server').app;describe('Server', () => {describe('GET /', () => {it('should respond with the correct status and response', (done) => {request(app).get('/').expect(200).expect('Test').end(done);});});describe('GET /err', () => {it('should respond with the 404 status and response as error: Page Not Found', (done) => {request(app).get('/err').expect(404).expect((res) => {expect(res.body).toInclude({error: 'Page Not Found!'});}).end(done);});});});
Above screenshot makes it quite clear how the tests are grouped and are easy to see in the logs too.
Test Spies
Sometimes, the function contains calls to function from other modules. In such cases, we need to make sure that the call to the other function was successful when our function was called. However, we don’t actually want to execute the other function’s code. In such case, we need to fake the other function. We use spies.
in spies/app.js
const db = require('./db');module.exports.signUp = (email, password) => {db.saveUser({email, password});};
in spies/db.js
module.exports.saveUser = (user) => {console.log(`Saving user ${user}.`);};
in spies/app.test.js
const expect = require('expect');it('should call the method once', () => {const spy = expect.createSpy(); // used when there is no function to spy on. tracks calls and argumentsspy();expect(spy).toHaveBeenCalled();});
However, we need to test that db.saveUser
gets called when app.signUp
is called.
We will be using rewire
which gives us two important things — __set__
and __get__
can be used to set variables from a module. Let’s see it in practice —
npm install --save-dev rewire
in spies/app.test.js
const expect = require('expect');const rewire = require('rewire');const app = rewire('./app');it('should call the method once', () => {const spy = expect.createSpy();spy();expect(spy).toHaveBeenCalled();});it('should call db.saveUser when app.signUp is called', () => {const db = {saveUser: expect.createSpy()};app.__set__('db', db);app.signUp();expect(db.saveUser).toHaveBeenCalled();});
Branch — 13_Init
This is pretty much what we will be covering in test cases for a node application.
In this part, we discussed about —
- testing
- mocha
- assertion library — expect
- testing async code
- testing express application — supertest
- spies
Thanks. See ya in next part :)
Part 6.1 — https://medium.com/@anujbaranwal/nodejs-8-from-scratch-part-6-1-api-development-5dee11785d62
Part 6.2 — https://medium.com/@anujbaranwal/nodejs-8-from-scratch-part-6-2-api-development-f92f76eb3521
Part 6.3 — https://medium.com/@anujbaranwal/nodejs-8-from-scratch-part-6-3-api-development-9b046fed7364
Part 6.4 — https://medium.com/@anujbaranwal/nodejs-8-from-scratch-part-6-4-api-development-38d600a35ad9