I am new to jest, sorry if this is a trivial question but I went through the official jest docs and I am not able to find a solution to the problem.
I am developing a very simple app in nodejs that consumes data from a websocket and propagates it downstream to a set of consumers via zeromq.
The code is the following:
app.js:
const initializer = require("./dependencyInitializer");
const sock = initializer.zmqSock();
const ws = initializer.wsClient();
ws.on('update', data => {
sock.send([data.topic, JSON.stringify(data)]);
});
The websocket client is a class from a third party library extending from EventEmitter.
I would like to create a test asserting that the sock.send function is called exactly once inside the handler of the 'update' event.
This is my approach:
app.spec.js:
const ws = require("./app");
const initializer = require("./dependencyInitializer");
jest.mock("./dependencyInitializer", () => {
return {
wsClient: jest.fn(() => {
const EventEmitter = require("events")
const emitter = new EventEmitter()
return emitter;
}),
zmqSock: jest.fn(() => {
return {
send: jest.fn()
}
})
}
});
describe('on message received from websocket',() => {
it('should pass it to zmq', () => {
const data = {result: "ok"};
expect(initializer.wsClient).toHaveBeenCalledTimes(1);
expect(initializer.zmqSock).toHaveBeenCalledTimes(1);
const _sock = initializer.zmqSock();
const _ws = initializer.wsClient();
_ws.emit("update", data);
expect(_sock.send).toHaveBeenCalledTimes(1);
});
});
The test fails with the following:
on message received from websocket › should pass it to zmq
expect(jest.fn()).toHaveBeenCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 0
28 | const _ws = initializer.wsClient();
29 | _ws.emit("update", data);
> 30 | expect(_sock.send).toHaveBeenCalledTimes(1);
| ^
31 | });
32 | });
I am not sure if I am on the right path, I would like to understand what would be the best approach to develop a test like this.
Thanks
After mocking by jest.mock(), when the .wsClient() and .zmqSock() methods are called in the app.js file and app.spec.js file, the sock and ws objects in app.js are different with the app.spec.js.
{
wsClient: jest.fn(() => {
const EventEmitter = require("events")
const emitter = new EventEmitter()
return emitter;
})
}
Every time you call .wsClient(), it will create a new object.
Emitter can only listen for events from its own emit. The solution is to create the mock emitter and sock objects in the mock factory.
app.js:
const initializer = require('./dependencyInitializer');
const sock = initializer.zmqSock();
const ws = initializer.wsClient();
ws.on('update', (data) => {
sock.send([data.topic, JSON.stringify(data)]);
});
module.exports = { sock, ws };
app.test.js:
const app = require('./app');
const initializer = require('./dependencyInitializer');
jest.mock(
'./dependencyInitializer',
() => {
const EventEmitter = require('events');
const emitter = new EventEmitter();
const mSock = { send: jest.fn() };
return {
wsClient: jest.fn(() => emitter),
zmqSock: jest.fn(() => mSock),
};
},
{ virtual: true }
);
describe('on message received from websocket', () => {
it('should pass it to zmq', () => {
const data = { result: 'ok' };
expect(initializer.wsClient).toHaveBeenCalledTimes(1);
expect(initializer.zmqSock).toHaveBeenCalledTimes(1);
const _sock = initializer.zmqSock();
const _ws = initializer.wsClient();
// check if they have same reference
expect(app.sock).toBe(_sock);
expect(app.ws).toBe(_ws);
_ws.emit('update', data);
expect(_sock.send).toHaveBeenCalledTimes(1);
});
});
test result:
PASS examples/70024105/app.test.js (9.279 s)
on message received from websocket
✓ should pass it to zmq (2 ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
app.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 10.112 s
Related
I am currently working on a full-stack application using the MERN stack. I have been writing unit tests for each route's request handler, and have been finding some difficulty testing error cases, particularly in trying to stub a function to reject a promise
I have the relevant code shown below:
One of my endpoints. Request handling is delegated to userController
const express = require("express");
const { body } = require("express-validator");
const router = express.Router();
const userController = require("../../controllers/user");
router.post(
"/",
body("username")
.isLength({
min: 3,
max: 30,
})
.withMessage(
"Your username must be at least 3 characters and no more than 30!"
),
body("password")
.isLength({ min: 3, max: 50 })
.withMessage(
"Your password must be at least 3 characters and no more than 50!"
),
userController.createNewUser
);
The request handler for the above endpoint. I am trying to test createNewUser. I want to stub createNewUser so that it causes an error to be thrown, so I can test that a 500 status code response is sent.
const bcrypt = require("bcryptjs");
const { validationResult } = require("express-validator");
const User = require("../models/User");
exports.createNewUser = async (req, res, next) => {
const { username, password } = req.body;
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array(),
});
}
try {
// Create a bcrypt salt
const salt = await bcrypt.genSalt(12);
// Hash the password
const hashedPassword = await bcrypt.hash(password, salt);
// Create a new user
const user = new User({
username,
password: hashedPassword,
});
const response = await user.save();
res.status(200).json(response);
} catch (err) {
res.status(500).json({ msg: err.message });
}
};
The unit test for User endpoints. I am unsure how to test the error case where a 500 status code is returned...
const request = require("supertest");
// const todosController = require("../controllers/todos");
const server = require("../server");
const User = require("../models/TodoItem");
const db = require("./db");
const agent = request.agent(server);
// Setup connection to the database
beforeAll(async () => await db.connect());
afterEach(async () => await db.clear());
afterAll(async () => await db.close());
describe("User endpoints test suite", () => {
describe("POST api/user", () => {
test("It should create a user successfully and return a 200 response code", async () => {
const response = await agent
.post("/api/user")
.set("content-type", "application/json")
.send({ username: "Bob", password: "12345" });
expect(response.body.username).toEqual("Bob");
expect(response.status).toBe(200);
});
});
});
When you are creating unit test, create something small first, you can add complexity and refactor later.
Below are example simple unit and integration tests based on your code.
You can start with user controller.
// File: user.controller.js
const bcrypt = require('bcryptjs');
exports.createNewUser = async (req, res) => {
try {
// Create a bcrypt salt.
const salt = await bcrypt.genSalt(12);
// Just make it simple, show the salt.
res.status(200).json(salt);
} catch (err) {
// Other wise, return the error message.
res.status(500).json({ msg: err.message });
}
};
Based on that try and catch, you can create unit test.
// File: user.controller.spec.js
const bcrypt = require('bcryptjs');
const user = require('./user.controller');
describe('User Controller', () => {
describe('create New User', () => {
const fakeJson = jest.fn();
const fakeStatus = jest.fn().mockReturnThis();
const fakeRes = {
status: fakeStatus,
json: fakeJson,
};
const spy = jest.spyOn(bcrypt, 'genSalt');
afterEach(() => {
jest.clearAllMocks();
});
it('should return salt', async () => {
const testSalt = 'salt';
// Mock the bcrypt.genSalt, always resolved with value testSalt.
spy.mockResolvedValue(testSalt);
// Call the function under test.
await user.createNewUser(undefined, fakeRes);
// Set the expectations.
expect(fakeStatus).toHaveBeenCalledWith(200);
expect(fakeJson).toHaveBeenCalledWith(testSalt);
expect(spy.mock.calls[0][0]).toBe(12);
});
it('should return error message when error', async () => {
const error = new Error('XXX');
// Mock the bcrypt.genSalt, always resolved with value testSalt.
spy.mockRejectedValue(error);
// Call the function under test.
await user.createNewUser(undefined, fakeRes);
// Set the expectations.
expect(fakeStatus).toHaveBeenCalledWith(500);
expect(fakeJson).toHaveBeenCalledWith({ msg: error.message });
expect(spy.mock.calls[0][0]).toBe(12);
});
});
});
When you run it on terminal:
$ npx jest user.controller.spec.js
PASS ./user.controller.spec.js
User Controller
create New User
✓ should return salt (5 ms)
✓ should return error message when error (1 ms)
--------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
user.controller.js | 100 | 100 | 100 | 100 |
--------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.511 s, estimated 1 s
Ran all test suites matching /user.controller.spec.js/i.
Next, if you have sure with your controller, you can create integration test with express.
For example you create app index like this.
// File: index.js
const express = require('express');
const userController = require('./user.controller');
const router = express.Router();
router.post('/user', (req, res, next) => userController.createNewUser(req, res, next));
const app = express();
app.use('/api', router);
module.exports = app;
You can test it using jest for normal & error case like this.
// File: index.spec.js
const request = require('supertest');
const bcrypt = require('bcryptjs');
const server = require('./index');
const userController = require('./user.controller');
const agent = request.agent(server);
describe('App', () => {
describe('POST /', () => {
// Create spy on bcrypt.
const spy = jest.spyOn(bcrypt, 'genSalt');
const error = new Error('XXX');
afterEach(() => {
jest.clearAllMocks();
});
it('should create a salt successfully and return a 200 response code', async () => {
// This test is slow because directly call bcrypt.genSalt.
// To make it faster, mock bcrypt completely, or use spy.mockResolvedValue('SALT');
// Send post request.
const response = await agent.post('/api/user');
// Make sure the response.
expect(response.status).toBe(200);
expect(response.type).toBe('application/json');
expect(spy.mock.results[0].value).toBeDefined();
const spyResult = await spy.mock.results[0].value;
expect(response.body).toBe(spyResult)
});
it('should return 500 and error message when catch error', async () => {
// Makesure spy reject.
spy.mockRejectedValue(error);
// Send post request.
const response = await agent.post('/api/user');
// Make sure the response.
expect(response.status).toBe(500);
expect(response.type).toBe('application/json');
expect(response.body).toBeDefined();
expect(response.body.msg).toBeDefined();
expect(response.body.msg).toBe(error.message);
});
// Or play around with another spy to error alternatives.
it('should return 404 when pass to next', async () => {
// Makesure createNewUser error.
jest.spyOn(userController, 'createNewUser').mockImplementation((req, res, next) => {
// You can setup res here or other implementation to check.
// For example, do next.
next();
});
// Send post request.
const response = await agent.post('/api/user');
// Make sure the response.
expect(response.status).toBe(404);
// Method bcrypt.genSalt should not get called.
expect(spy).not.toHaveBeenCalled();
});
});
});
When you run it from terminal:
$ npx jest index.spec.js
PASS ./index.spec.js
App
POST /
✓ should create a salt successfully and return a 200 response code (40 ms)
✓ should return 500 and error message when catch error (4 ms)
✓ should return 404 when pass to next (5 ms)
--------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.js | 100 | 100 | 100 | 100 |
user.controller.js | 100 | 100 | 100 | 100 |
--------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 0.809 s, estimated 1 s
Ran all test suites matching /index.spec.js/i.
Note: You do not need to use sinon, jest provides mock functions.
I'm new to writing mocha unit tests and would like to ask how the unit test for the following code would look like (especially for the BigQuery part). The code is in a PubSub triggered Cloud Function and inserts rows into a BigQuery table:
/**
* Triggered from a message on a Cloud Pub/Sub topic.
*
* #param {!Object} event Event payload.
* #param {!Object} context Metadata for the event.
*/
const {BigQuery} = require('#google-cloud/bigquery');
const bigqueryClient = new BigQuery();
const dataset = 'dataset_name';
const table = 'table_name';
exports.sendtobigquery = (event, context) => {
const pubsubMessage = Buffer.from(event.data, 'base64').toString();
BigQueryInsert(pubsubMessage, dataset, table);
};
async function BigQueryInsert(pubsubMessage, dataset, table) {
const date = new Date().toISOString(); // new date
const rows = [{field1: pubsubMessage, field2: date}]; // new field2
await bigqueryClient
.dataset(dataset)
.table(table)
.insert(rows);
}
I've seen here Mocking ES6 BigQuery class that sinon and proxyquire should be used but I don't understand how to do that for my code exactly.
Any help is appreciated.
Update:
I added a new date field as part of the row but I'm having trouble here too when it comes to the unit test. For the date I tried using sinon.useFakeTimers in the index.test.js from the answer like that
describe('66267929', () => {
const now = new Date();
const date = sinon.useFakeTimers(now.getTime());
beforeEach(() => {
sinon.restore();
});
afterEach(() => {
sinon.restore();
});
it('should pass', async () => {
... // same code
... // same code
sinon.assert.calledWithExactly(bigqueryClientStub.insert, [{ field1: 'teresa teng', field2: date }]);
});
});
but that resulted in "AssertError: expected stub to be called once but was called 0 times". How can this be done?
Since you call the BigQueryInsert function without async/await, we need to flush the promise queue to ensure that all asynchronous method calls have been completed on the bigqueryClient object.
We use proxyquire and sinonjs to stub the BigQuery constructor.
We use returnsThis() to achieve the chain methods call.
E.g.
index.js:
const { BigQuery } = require('#google-cloud/bigquery');
const bigqueryClient = new BigQuery();
const dataset = 'dataset_name';
const table = 'table_name';
exports.sendtobigquery = (event, context) => {
const pubsubMessage = Buffer.from(event.data, 'base64').toString();
BigQueryInsert(pubsubMessage, dataset, table);
};
async function BigQueryInsert(pubsubMessage, dataset, table) {
const rows = [{ field1: pubsubMessage }];
await bigqueryClient.dataset(dataset).table(table).insert(rows);
}
index.test.js:
const proxyquire = require('proxyquire');
const sinon = require('sinon');
const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
describe('66267929', () => {
afterEach(() => {
sinon.restore();
});
it('should pass', async () => {
const bigqueryClientStub = {
dataset: sinon.stub().returnsThis(),
table: sinon.stub().returnsThis(),
insert: sinon.stub().resolves(),
};
const googleCloundBigqueryStub = {
BigQuery: sinon.stub().returns(bigqueryClientStub),
};
const { sendtobigquery } = proxyquire('./', {
'#google-cloud/bigquery': googleCloundBigqueryStub,
});
const data = Buffer.from('teresa teng').toString('base64');
sendtobigquery({ data });
await flushPromises();
sinon.assert.calledOnce(googleCloundBigqueryStub.BigQuery);
sinon.assert.calledWithExactly(bigqueryClientStub.dataset, 'dataset_name');
sinon.assert.calledWithExactly(bigqueryClientStub.table, 'table_name');
sinon.assert.calledWithExactly(bigqueryClientStub.insert, [{ field1: 'teresa teng' }]);
});
});
unit test result:
66267929
✓ should pass (343ms)
1 passing (346ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
I'm trying to mock the crypto module of nodejs with the jestjs framework.
Here is the piece of code that I want to mock:
app.js:
const hash = crypto
.createHmac('sha256', API_TOKEN)
.update(JSON.stringify(body))
.digest('hex');
if(hash === signature) {
//verified successfully. Implement next logic
}
Here, I want the .digest function to give the value contained in the signature variable.
Following is the mock code:
jest.mock('crypto', () => {
return {
createHmac: jest.fn(() => ({
update: jest.fn(),
digest: jest.fn(() => '123') //here '123' is placeholder for 'signature value
}))
};
});
However, when running the test, the main files throws an error like this:
TypeError: Cannot read property 'update' of undefined
What am I missing here for jest mock?
You should use mockFn.mockReturnThis() to obtain method chain call.
E.g.
app.js:
const crypto = require('crypto');
function main() {
const API_TOKEN = 'API_TOKEN';
const signature = '123';
const body = {};
const hash = crypto
.createHmac('sha256', API_TOKEN)
.update(JSON.stringify(body))
.digest('hex');
if (hash === signature) {
console.log('verified successfully. Implement next logic');
}
}
module.exports = main;
app.spec.js:
const main = require('./app');
const crypto = require('crypto');
jest.mock('crypto', () => {
return {
createHmac: jest.fn().mockReturnThis(),
update: jest.fn().mockReturnThis(),
digest: jest.fn(() => '123'),
};
});
describe('64386858', () => {
it('should pass', () => {
const logSpy = jest.spyOn(console, 'log');
main();
expect(crypto.createHmac).toBeCalledWith('sha256', 'API_TOKEN');
expect(crypto.update).toBeCalledWith(JSON.stringify({}));
expect(crypto.digest).toBeCalledWith('hex');
expect(logSpy).toBeCalledWith('verified successfully. Implement next logic');
logSpy.mockRestore();
});
});
unit test result:
PASS src/stackoverflow/64386858/app.spec.js
64386858
✓ should pass (17ms)
console.log node_modules/jest-environment-jsdom/node_modules/jest-mock/build/index.js:866
verified successfully. Implement next logic
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 100 | 50 | 100 | 100 | |
app.js | 100 | 50 | 100 | 100 | 13 |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 5.087s, estimated 11s
I am trying to unit test the below listEntities function by mocking runQuery and createQuery functions. Maybe I should just give up and do an integration test with an emulator. Anyway, here is my code
Implementation:
const Datastore = require('#google-cloud/datastore');
const datastore = Datastore();
const runQueryDS = (query) => datastore.runQuery(query);
const createQueryDS = (kind) => datastore.createQuery(kind);
export const listEntities = (kind, runQuery = runQueryDS, createQuery = createQueryDS) => {
console.log('listEntities');
const query = createQuery(kind);
runQuery(query).then((results) => results[0]);
};
Test:
import { listEntities } from './datastore.api';
describe('datastore api', () => {
describe('listEntities', () => {
test('should return list of items', () => {
console.log('begin test');
const kind = 'TestRun';
const createdQuery = 'createdQuery';
const expectedResult = ['returnedFromQuery'];
const returnedFromExecutedQuery = [expectedResult];
const createQuery = jest.fn().mockImplementation(() => (createdQuery));
const runQuery = jest.fn().mockImplementation(() => (returnedFromExecutedQuery));
const result = listEntities(kind, runQuery, createQuery);
expect(result).toEqual(expectedResult);
});
});
});
This is the error I get
FAIL app/datastore.api.test.js
● Test suite failed to run
Cannot find module './datastore_client_config' from 'datastore_client.js'
at Resolver.resolveModule (node_modules/jest-resolve/build/index.js:191:17)
at Object.<anonymous> (node_modules/#google-cloud/datastore/src/v1/datastore_client.js:30:18)
Thank you!
Unit testing should be completed first, followed by integration testing. Unit tests are easier to write than integration tests, and have good isolation, do not rely on external services, have no side effects, and can run in different environments.
Here is the unit test solution:
index.js:
const Datastore = require('#google-cloud/datastore');
const datastore = Datastore();
const runQueryDS = (query) => datastore.runQuery(query);
const createQueryDS = (kind) => datastore.createQuery(kind);
const listEntities = (kind, runQuery = runQueryDS, createQuery = createQueryDS) => {
console.log('listEntities');
const query = createQuery(kind);
return runQuery(query).then((results) => results[0]);
};
module.exports = { listEntities };
index.test.js:
const { listEntities } = require('./');
const Datastore = require('#google-cloud/datastore');
jest.mock('#google-cloud/datastore', () => {
const mDatasotre = {
runQuery: jest.fn(),
createQuery: jest.fn(),
};
return jest.fn(() => mDatasotre);
});
describe('47128513', () => {
describe('#listEntities', () => {
afterAll(() => {
jest.resetAllMocks();
});
it('should list entities', async () => {
const mDatastore = Datastore();
mDatastore.createQuery.mockReturnValueOnce('fake query');
mDatastore.runQuery.mockResolvedValueOnce([{ id: 1 }]);
const actual = await listEntities('kind');
expect(actual).toEqual({ id: 1 });
expect(mDatastore.createQuery).toBeCalledWith('kind');
expect(mDatastore.runQuery).toBeCalledWith('fake query');
});
});
});
unit test result with coverage report:
PASS src/stackoverflow/47128513/index.test.js (12.111s)
47128513
#listEntities
✓ should list entities (12ms)
console.log src/stackoverflow/47128513/index.js:355
listEntities
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
index.js | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 13.865s, estimated 15s
source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/47128513
What would be the proper way to unit test this getall function using mocha/chai? I'm having a hard time understanding what to expect with Promise.all.
const Promise = require('bluebird');
const someApiService = require('./someapiservice');
const _ = require('underscore');
function getall(arr) {
let promises = _.map(arr, function(item) {
return someApiService(item.id);
});
return Promise.all(promises);
}
We should stub standalone function someApiService use link seams. This is the CommonJS version, so we will be using proxyquire to construct our seams. Additionally, I use sinon.js to create the stub for it.
E.g.
getall.js:
const Promise = require('bluebird');
const someApiService = require('./someapiservice');
const _ = require('underscore');
function getall(arr) {
let promises = _.map(arr, function (item) {
return someApiService(item.id);
});
return Promise.all(promises);
}
module.exports = getall;
someapiservice.js:
module.exports = function someApiService(id) {
return Promise.resolve('real data');
};
getall.test.js:
const proxyquire = require('proxyquire');
const sinon = require('sinon');
const { expect } = require('chai');
describe('45337461', () => {
it('should pass', async () => {
const someapiserviceStub = sinon.stub().callsFake((id) => {
return Promise.resolve(`fake data with id: ${id}`);
});
const getall = proxyquire('./getall', {
'./someapiservice': someapiserviceStub,
});
const actual = await getall([{ id: 1 }, { id: 2 }, { id: 3 }]);
expect(actual).to.deep.equal(['fake data with id: 1', 'fake data with id: 2', 'fake data with id: 3']);
sinon.assert.calledThrice(someapiserviceStub);
});
});
unit test result:
45337461
✓ should pass (2745ms)
1 passing (3s)
-------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------------|---------|----------|---------|---------|-------------------
All files | 88.89 | 100 | 66.67 | 88.89 |
getall.js | 100 | 100 | 100 | 100 |
someapiservice.js | 50 | 100 | 0 | 50 | 2
-------------------|---------|----------|---------|---------|-------------------