How to unit test an Express Controller with Jest - javascript

I have been getting terribly confused with how to test my controller functions. I realize that I have to mock my dependencies, request, response, and the controller functions. Here's what I have so far:
OrdersController.js
const OrderService = require('../services/orderServices')
module.exports = class OrdersController {
static async apiGetOrders(req, res, next) {
try {
const orders = await OrderService.getOrders()
return res.status(200).json(orders)
} catch (error) {
return res.status(500).json({ error: 'Unable to get orders' }) // 500, Internal Service Error, generic
}
}
static async apiPostOrder(req, res, next) {
// All good, create an orderDocument
try {
const orderDocument = {
_id: null, // undefined at this point Mongo creates this _id for us
orderId: req.body.orderId,
cookies: req.body.cookies,
daySelected: req.body.daySelected,
timeSelected: req.body.timeSelected,
userInfo: req.body.userInfo,
createdAt: new Date(),
}
await OrderService.addOrder(orderDocument)
return res.status(201).send('success') // status OK, something was Created
} catch (error) {
return res.status(500).json({ error }) // 500, Internal Server Error
}
}
OrdersController.spec.js
import OrderService from '../services/orderServices'
import { mockOrder, mockOrders } from '../mocks/fixtures'
import OrdersController from '../controllers/ordersController'
jest.mock('../controllers/ordersController.js')
const mockRequest = () => {
return {}
}
const mockResponse = (mockOrders) => {
const res = {};
res.status = jest.fn().mockReturnValue(200);
res.json = jest.fn().mockReturnValue(mockOrders);
return res;
}
// #3 Test the OrdersControllers
// mock dependencies: req, res, and spyOn the controller functions
describe('Orders Controller', () => {
test('[Positive], should call OrderService.getOrders and receive status 200', async () => {
jest.spyOn(OrdersController, 'apiGetOrders')
const req = mockRequest()
const res = mockResponse(mockOrders)
await OrdersController.apiGetOrders(req, res)
expect(res.status).toHaveBeenCalledWith(200)
expect(res.json()).toEqual(mockOrders)
})
test('[Negative], error yields status 500', async () => {
jest.spyOn(OrdersController, 'apiGetOrders')
const req = mockRequest()
const res = mockResponse({status: 500, error: 'Unable to get orders'})
await OrdersController.apiGetOrders(req, res)
expect(res.status).toHaveBeenCalledWith(500)
expect(res.json()).toEqual(error)
})
})
I'm trying to test the happy path and the negative path on the get request. I followed this expample, https://codewithhugo.com/express-request-response-mocking/, and read all of the jest docs, https://jestjs.io/docs/mock-functions. The error that I receive is:
Questions:
Am I actually writing the tests correctly?
Am I also supposed to mock the OrderService?
Should I use Sinon or is Jest more than sufficient?

I am also new to Jest and am struggling with finding documentation that goes to enough detail to suggest to me what I'm doing wrong. But in your case, I think you might be spying on the wrong thing. The OrdersController is the subject of the test, so I don't believe that you should be mocking that. Rather you should spy on OrdersController's dependency, Orderservice and mock its methods.
Also, its not clear to me why you have next in
static async apiGetOrders(req, res, next)
You don't seem to use it in the body of the method anywhere, so hanging any testing off that value, probably won't work either.

Related

How to deal with async functions in express js router

There is a lint error, that basically says that we cannot return Promises in places where a void is expected, the message is clear, but how to fix it without lying to the linter by using any or (as any), both functions [validateJWT, getUser] are async functions It looks pretty basic, but I do not know how to fix it in an easy way. thanks!
import { Router } from 'express';
import { getUser } from '../controllers/userController';
import { validateJWT } from '../middlewares/validateJWT';
const router = Router();
router.get('/user', validateJWT, getUser);
export default router;
const getUser = async (req: Request, res: Response, next:
NextFunction): Promise<any> => {
try {
const { id } = req.params;
if (!id) {
let response = formatErrorResponse({ error: true, statusCode: 400, errorMessage: 'Missing id in params' });
return res.status(400).json(response);
}
let user = await User.findById({_id: id});
let objRes = { error: false, statusCode: 200, message: 'User was found', data: user };
return res.status(200).json(objRes);
} catch (error) {
console.log('error in controller /user', error)
next(error);
}
}
export {
getUser
}
const validateJWT = async (req: Request, res: Response, next: NextFunction): Promise<any> => {
const token = req.header('x-token');
console.log(req)
if (!token) {
const err = formatErrorResponse({ error: true, statusCode: 401, errorMessage: 'Missing header x-token' });
return res.status(401).json(err);
}
try {
await verifyToken(token);
next();
} catch (error) {
const err = formatErrorResponse({error: true, statusCode: 401, errorMessage: 'Invalid token, athorization denied', errorContent: error});
return res.status(400).json(err);
}
}
You need to change your implementation of the function to work like this:
router.get('/user', (req, res) => {
validateJWT(req, res);
getUser(req, res);
});
Since express.Router instances expect the route as a string for the first parameter which you have as "/user". And a second parameter which is a callback.
Inside that callback you can call your functions.
Assuming you need validateJWT to finish running before getUser you could do something like this:
validateJWT(...).then((...) => getUser(...));
I suggest something like the below.
router.use(validateJWT)
router.get('/users/:id', (req, res, next) => {
getUser(req.params.id)
.then(res.json)
.catch(next);
});
In the docs, I could see an async middleware example http://expressjs.com/en/guide/writing-middleware.html#mw-fig.
I could not find an async handler example. I recall that back in the day, express didn't support async handler. Hence, I used .then inside the handler. You need to double-check if you can use them nowadays.
In the express docs is also an example like the below. It makes working with async handler more convenient in some regard. As you can wrap all your async handler with this wrapper.
const wrap = fn => (...args) => fn(...args).catch(args[2]);
router.get('/users/:id', wrap(getUser));

BeforeAll in JEST not working as expected with asynchronous code

I'm trying to write few test cases using JEST for testing my API's. So I need the JWT token value in these cases. For getting the token value I've created an asynchronous getJWTToken function. Now I'm calling this function inside beforeAll().
But I'm getting random results on running the test cases. Most of the times all test cases are getting passed successfully but few of the times I'm getting 403 forbidden error as before getting the Token value my test cases started processing.
Could someone suggest any workaround for this.?
import app from '../../../src/app';
const { getJWTToken } = require('../../../src/utils/app.util');
const should = require('should');
const request = require('supertest')(app);
describe('API Test Cases :', () => {
const body = {};
beforeAll(async () => {
const data = await getJWTToken();
body['user_id'] = data[0]['_id'];
body['token'] = data[0]['token'];
});
describe('API 1', () => {
it('Should fetch', (done) => {
request.post('endpoint')
.send({
requestBody
})
.set('Accept', 'application/json')
.set('Authorization', `bearer ${body['token']}`)
.expect(commonAssertions)
.end((err, res) => {
if (err) return done(err);
return done();
});
});
});
}

JestJS: Async test isn't stopped

I got two problems with this jest test:
Is it possible to define the Content collection only once instead of doing that inside of the test?
I do get this error:
Jest did not exit one second after the test run has completed.
This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with --detectOpenHandles to troubleshoot this issue.
I don't see why my async code weren't stopped...
import resolvers from 'resolvers/'
import Db from 'lib/db'
const db = new Db()
describe('Resolver', () => {
let token
beforeAll(async () => {
await db.connect()
})
beforeEach(async () => {
token = 'string'
await db.dropDB()
})
afterAll(async () => {
await db.connection.close()
})
describe('articleGetContent()', () => {
test('should return dataset', async () => {
// SETUP
const Content = db.connection.collection('content')
const docs = [{
// some content...
}]
await Content.insertMany(docs)
// EXECUTE
const result = await resolvers.Query.articleGetContent({}, {
id: '123,
language: 'en'
}, {
token
})
// VERIFY
expect.assertions(1)
expect(result).toBeDefined()
})
})
})
resolver
import { articleGetContent } from '../models/article'
export default {
Query: {
articleGetContent: async (obj, { id }, { token }) => articleGetContent(id, token)
}
}
This is how my db class looks like
db.js
export default class Db {
constructor (uri, callback) {
const mongo = process.env.MONGO || 'mongodb://localhost:27017'
this.mongodb = process.env.MONGO_DB || 'testing'
this.gfs = null
this.connection = MongoClient.connect(mongo, { useNewUrlParser: true })
this.connected = false
return this
}
async connect (msg) {
if (!this.connected) {
try {
this.connection = await this.connection
this.connection = this.connection.db(this.mongodb)
this.gfs = new mongo.GridFSBucket(this.connection)
this.connected = true
} catch (err) {
console.error('mongo connection error', err)
}
}
return this
}
async disconnect () {
if (this.connected) {
try {
this.connection = await this.connection.close()
this.connected = false
} catch (err) {
console.error('mongo disconnection error', err)
}
}
}
async dropDB () {
const Content = this.connection.collection('content')
await Content.deleteMany({})
}
}
Related to the second question I hope you've found some issues on github about it.
In general, the issue is described in the debug log.
Jest works with promises, as a result, you shouldn't leave any async operations in any status except resolved.
In your case, you have your DB connection opened so you need to implement another method disconnect for your DB class, this link to docs will help you, but I guess you have it already as it's not the full db.js file ( I see some custom method dropDB. Main idea here is to have it in afterAll hook:
afterAll(() => db.disconnect());
Great example at the bottom of the page
What about the first question, it really depends on what you are doing in your method dropDB. If you're running method for dropping collection, you could store the reference to this collection somewhere outside and use it as it will automatically create the new one, but it would be great to see this method.
Additionally, your async test was created in a wrong way, you could read more here for example in my Update. You need to run this function in the beginning of the test: expect.assertions(number)
expect.assertions(number) verifies that a certain number of assertions
are called during a test. This is often useful when testing
asynchronous code, in order to make sure that assertions in a callback
actually got called.

JEST error TypeError: specificMockImpl.apply is not a function

Trying to mock one of the function with callback from api and getting error as TypeError: specificMockImpl.apply is not a function
import { IEnvironmentMap, load } from 'dotenv-extended';
import { getTokensWithAuthCode, sdk } from '../src/connection-manager';
describe('getTokensWithAuthCode function Tests', () => {
jest.useFakeTimers();
let boxConfig: IEnvironmentMap;
beforeAll(() => {
boxConfig = load({
errorOnMissing: true,
});
});
it('should reject a promise if there is wrong auth code provided', async () => {
sdk.getTokensAuthorizationCodeGrant = jest.fn().mockImplementation(boxConfig.BOX_AUTH_CODE, null, cb => {
cb('Error', null);
});
try {
const tokens = await getTokensWithAuthCode();
} catch (error) {
expect(error).toBe('Error');
}
});
});
And my function which is trying to test is as follow:
import * as BoxSDK from 'box-node-sdk';
import { IEnvironmentMap, load } from 'dotenv-extended';
import {ITokenInfo} from '../typings/box-node-sdk';
const boxConfig: IEnvironmentMap = load({
errorOnMissing: true,
});
export const sdk: BoxSDK = new BoxSDK({
clientID: boxConfig.BOX_CLIENT_ID,
clientSecret: boxConfig.BOX_CLIENT_SECRET,
});
/**
* - Use the provided AUTH_CODE to get the tokens (access + refresh)
* - Handle saving to local file if no external storage is provided.
*/
export async function getTokensWithAuthCode() {
return new Promise((resolve: (tokenInfo: ITokenInfo) => void, reject: (err: Error) => void) => {
if (boxConfig.BOX_AUTH_CODE === '') {
reject(new Error('No Auth Code provided. Please provide auth code as env variable.'));
}
sdk.getTokensAuthorizationCodeGrant(boxConfig.BOX_AUTH_CODE, null, (err: Error, tokenInfo: ITokenInfo) => {
if (err !== null) {
reject(err);
}
resolve(tokenInfo);
});
});
}
Is there any other way to mock function in jest? I have read an article https://www.zhubert.com/blog/2017/04/12/testing-with-jest/
On this line, rather than pass a function to mockImplementation, you're passing three arguments:
jest.fn().mockImplementation(boxConfig.BOX_AUTH_CODE, null, cb => {
cb('Error', null);
});
It looks like you might have just missed some braces. Try switching it to:
jest.fn().mockImplementation((boxConfig.BOX_AUTH_CODE, null, cb) => {
cb('Error', null);
});
It's better not trying to mutate a const used elsewhere.
You could change getTokensWithAuthCode to make it receive sdk as parameter, thus in your test you would pass the mock function as argument, therefore having a more predictable behavior than mutating directly sdk.
In your code, you could make a second getTokensWithAuthCode implementation, with the signature getTokensWithAuthCodeUnbound(sdk) for example, and export it. This implementation will be used in your tests.
Exporting using the same getTokensWithAuthCode name, you would call:
export const getTokensWithAuthCode = getTokensWithAuthCodeUnbound.bind(null, sdk)
That way, your app will use getTokensWithAuthCodeUnbound bound with the default sdk, and you can test more easily its implementation.
Mozilla Developer Network (MDN) bind documentation.

How to properly test an Express controller method with Mocha, Chai, and Sinon

I'm pretty new to using Sinon. I have the following test I've written, and it fails because res.status always comes back as not called.
import chai from 'chai';
import 'chai/register-should';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import { db } from '../../models';
import * as loginController from '../../controllers/login';
chai.use(sinonChai);
describe('Login controller', () => {
describe('post function', () => {
let findOne, req, status, send, res;
beforeEach(() => {
findOne = sinon.stub(db.User, 'findOne');
findOne.resolves(null);
req = { body: { email: 'test#test.com', password: 'testpassword' }};
status = sinon.stub();
send = sinon.spy();
res = { send: send, status: status };
status.returns(res);
loginController.post(req, res);
});
afterEach(() => {
findOne.restore();
});
it('should return a 401 status for an invalid email', (done) => {
res.status.should.be.calledWith(401);
findOne.restore();
done();
});
});
});
The method in the controller right now is pretty simple. It uses a sequelize findOne method first. If it doesn't find a matching email it should throw a 401. Here's what that looks like:
export function post(req,res) {
const email = req.body.email;
const password = req.body.password;
db.User.findOne({
where: {email: email}
}).then(user => {
if (user) {
// Other stuff happens here
} else {
res.status(401).send('That email address does not exist in our system.');
}
}).catch((error) => {
res.status(500).send(error.message);
});
}
When I run the test it does get to the else statement where it should be returning the status, but the test fails and when I check the log it says that the res.status wasn't ever called.
The problem here is that the spec is synchronous and doesn't take a promise into account.
It makes sense to return a promise for testability reasons:
export function post(req,res) {
...
return db.User.findOne(...)
...
}
This can be naturally done if route handler is async function.
Since Mocha supports promises, the specs can use async functions instead of done callback as well:
it('should return a 401 status for an invalid email', async () => {
const handlerResult = loginController.post(req, res);
expect(handlerResult).to.be.a('promise');
await handlerResult;
res.status.should.be.calledWith(401);
});

Categories