Dec 18, 2017

Node.js: REST API Testing using Mocha, Sinon and Chai

In my recent post, I covered how to implement token based authentication using Passport, JWT and bcrypt. Let's extend this post and look into testing REST APIs or server side methods in Node.js using Mocha, Chai and Sinon.

Mocha: It is a test runner to execute our tests.

keywords in code = Describe, It, before, after...etc

Chai: It is an assertion library and allows to write code like writing regular english.

keywords in code = should, assert..etc.

Sinon: It provides test spies (fake functions to track executions), stubs (replace functions with our fake implementation) and mocks (predefined fake method with behaviour).

I would strongly recommend to read the below post first:

Token Based Authentication with Node.js, Express, Mongoose and Passport

Environment:

Node 8.11.3

NPM 5.6.0

mongoose 5.2.x

express: 4.16.0

sinon 6.1.5

chai 4.1.2

supertest 3.1.0

mocha 5.2.0

Setup Packages

Let's continue to implementation in the same project. run following command to install packages


npm install mocha chai sinon supertest --save-dev

Common Test Methods

Add a Tests folder and create common.test.js. It will have common parameters and methods needed in test files.


process.env.NODE_ENV = "test";
process.env.API_BASE = "/api";

import User from  "../models/user.model";
import express from "../app";

export const request = require("supertest")(express);
export const chai = require("chai");
export const should = chai.should();

const defaultUser = { "username": "test@techbrij.com", "password": "test" };

const createUser = async () => {
    const UserModel = new User(defaultUser);
    await UserModel.save();
};

const getDefaultUser = async () => {
    let users = await User.find({ "username" : defaultUser.username });
    if (users.length === 0) {
        await createUser();
        return getDefaultUser();
    } else {
        return users[0];
    }
};

export const loginWithDefaultUser = async () => {
    let user = await getDefaultUser();
    return request.post(process.env.API_BASE + "/auth/signin")
        .send({ "username": defaultUser.username, "password": defaultUser.password })
        .expect(200);
};

export const cleanExceptDefaultUser = async () => {
    let user = await getDefaultUser();
    await User.deleteMany({ "username": {$ne: user.username}});    
};

For testing, we set a default user and some methods related to operation with it.

Auth APIs testing

Add user.test.js with following code to test user registration and login APIs:


import { request, loginWithDefaultUser, cleanExceptDefaultUser  } from "./common.test";

describe("# Auth APIs", () => {
    const apiBase = process.env.API_BASE || '/api';
    const newUser = { "username": "test-new@techbrij.com", "password": "test" };
      it("should create user", () => {
        return cleanExceptDefaultUser().then(() => {
            return request.post(apiBase + '/auth/signup')
                .send(newUser)
                .expect(200)
                .then(res => {                   
                    res.body.success.should.be.true;                
                });
        });
    });

    it("should retrieve the token", () => {
        return cleanExceptDefaultUser().then(res => {
            return loginWithDefaultUser().then(res => {
                res.status.should.equal(200);
                res.body.success.should.be.true;
                res.body.token.should.not.be.empty;
            });
        });
    });

    it("should not login with the right user but wrong password", () => {
        return request.post(apiBase + '/auth/signin')
            .send({ "username": newUser.username, "password": "random" })
            .expect(401);
    });

    it("should return invalid credentials error", () => {
        return request.post(apiBase + '/auth/signin')
            .send({ "username":  newUser.username, "password": "" })
            .expect(401)
            .then(res => {
                return request.post(apiBase + '/auth/signin')
                    .send({ "username":  newUser.username, "password": "mypass" })
                    .expect(401);
            });
    });
});

Project APIs testing

Similarly, add project.test.js to test project related APIs


import { request, loginWithDefaultUser,chai } from "./common.test";
import { cleanCollection } from "../models/project.model";
describe("# Project APIs", () => {
    const apiBase = process.env.API_BASE || '/api';
    const newProject = { title: 'Project-1',
        summary: 'This is summary.',
        description: 'This is description',
        submitDate: new Date()}
    let should = chai.should(); 
    let token;

    before(async ()=> {        
        //get token
        let resToken =  await loginWithDefaultUser();
        token = resToken.body.token;       

    })    
    it("should save the project", () => {       
            return request.post(apiBase + "/project")
                .set("Authorization", token)
                .send(newProject)
                .expect(200)
                .expect(res => {
                    res.body.success.should.be.true;
                    res.body.msg.should.equal("New project is created successfully.");
                })
    });

    it("should get list of projects", () => {
        return cleanCollection().then(()=>{
            return request.post(apiBase + "/project")
            .set("Authorization", token)
            .send(newProject)
            .expect(200)
            .then(()=>{
                return request.get(apiBase + "/project")
                            .set("Authorization", token)    
                            .send()       
                            .expect(200)
                            .expect(res =>{                
                                res.body.should.be.an('array').to.have.lengthOf(1);    
                                let item = res.body[0];                            
                                item.should.have.property('title').to.equal(newProject.title);
                                item.should.have.property('summary').to.equal(newProject.summary);
                                item.should.have.property('description').to.equal(newProject.description);
                            }); 
                        })
        })         

    });


      it("should return 401 with expired token", () => {
        return request.post(apiBase + "/project")
            .set("Authorization", "JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1YjcwMWQyNGQ2ZjIyYTJiZThiYjg1MzYiLCJ1c2VybmFtZSI6InRlc3RAdGVjaGJyaWouY29tIiwicGFzc3dvcmQiOiIkMmEkMTAkTEJNQy5tQVFxWWNmLjVZSlRlSVNlT1cvUVp1NWJ5WVN4anJmSGFQUTJZZVlkWXR6Y25lbFMiLCJfX3YiOjAsImlhdCI6MTUzNDQzODk0MywiZXhwIjoxNTM0NDM5MDYzfQ.zFMsJiny3At6vJRsjl8AzKnjlTMGVc1fdZnH2kwu6dQ")
            .send(newProject)
            .expect(res => {               
                res.body.message.should.equal("Your token has expired.")
            })
            .expect(401);
    });
});

Configuration

As babel is used to translate code ES6 to ES5 so add following in npm scripts of package.json


 "test": "npm run clean && \"./node_modules/.bin/mocha\" --timeout 5000 --require babel-core/register \"./{,!(node_modules)/**/}*.test.js\""

Now run following command to run tests:


npm run test

You will get following output:

REST API TEST MOCHA

Stub to Skip Auth Middleware

In above tests, we created a user realtime, login to get token and use that token to access the project. You might want to skip authentication middleware for testing. We will use Sinon Stub to skip it. Before this let's understand how auth middleware implemented in config/Passport


authenticate(req, res, next){
        passport.authenticate('jwt', { session: false}, (err, user, info) => {
          if (err) { return next(err); }
          if (!user) {
              if (info.name === "TokenExpiredError") {
                  return res.status(401).json({ message: "Your token has expired." });
              } else {
                  return res.status(401).json({ message: info.message });
              }
          }
          req.user = user;
          return next();
        })(req, res, next);
      };

and this method is called in routing before getting controller method called. I am creating a stub to skip it.


sinon.stub(passport,"authenticate").callsFake((strategy, options, callback) => {            
               callback(null, { "username": "test@techbrij.com"}, null);             
               return (req,res,next)=>{};
            });

Let us do project tests with skipping original Passport middleware. Add a file project-sinon.test.js


import { request,chai } from "./common.test";
import sinon from "sinon";
import passport from 'passport';
import { cleanCollection } from "../models/project.model";

describe("# Project APIs Test with Sinon", () => {
    const apiBase = process.env.API_BASE || '/api';
    const newProject = { title: 'Project-1',
        summary: 'This is summary.',
        description: 'This is description',
        submitDate: new Date()}
    let should = chai.should(); 

    before(async ()=> {  
        let passportStub =  sinon.stub(passport,"authenticate").callsFake((strategy, options, callback) => {            
               callback(null, { "username": "test@techbrij.com"}, null);             
               return (req,res,next)=>{};
            });
    })
    after(() => {
        passport.authenticate.restore();
    });    
    it("should save the project", () => {       
            return request.post(apiBase + "/project")             
                .send(newProject)
                .expect(200)
                .expect(res => {                   
                    res.body.success.should.be.true;
                    res.body.msg.should.equal("New project is created successfully.");
                })
    });

    it("should get list of projects", () => {
        return cleanCollection().then(()=>{
            return request.post(apiBase + "/project")           
            .send(newProject)
            .expect(200)
            .then(()=>{
                return request.get(apiBase + "/project")                             
                            .send()       
                            .expect(200)
                            .expect(res =>{                                              
                                res.body.should.be.an('array').to.have.lengthOf(1);    
                                let item = res.body[0];                            
                                item.should.have.property('title').to.equal(newProject.title);
                                item.should.have.property('summary').to.equal(newProject.summary);
                                item.should.have.property('description').to.equal(newProject.description);
                            }); 
                        })
        })         

    });



});

Here is the result:

REST API TEST MOCHA

Stubs are really great and it helps a lot to test a complex function with complex dependencies.

Source Code

The full source code is available on GitHub.

Conclusion

This tutorial covers:

- How to test REST APIs using Mocha, Chai and Supertest

- How to manage authentication and token for testing

- How to use Sinon stub to skip Passport authenticate

- How to run Mocha ES6 test with Babel

If you have any suggestion or question, feel free to leave a comment below.

Enjoy Node.js!