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:
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:
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!