Unit test with sinon fake does not resolve promise - node.js

I'm learning nodejs and wrote this wrapper for a shelljs function, which in practice seems to work as intended.
/**
* Wrapper for Shelljs.exec to always return a promise
*
* #param {String} cmd - bash-compliant command string
* #param {String} path - working directory of the process
* #param {Object} _shell - alternative exec function for testing.
* #returns {String}
* #throws {TypeError}
*/
function shellExec(cmd, path, _shell = shelljs){
if( typeof _shell.exec !== "function") throw new TypeError('_shell.exec must be a function');
return new Promise((resolve, reject) => {
let options = { cwd: path, silent: true, asyc: true }
// eslint-disable-next-line no-unused-vars
return _shell.exec(cmd, options, (code, stdout, stderr) => {
// shelljs.exec does not always return a code
if(stderr) {
return reject(stderr);
}
return resolve(stdout);
});
});
}
However when I attempt to unit test it, the function times out. I have read the mochajs docs about async code, promises or async/await in tests. I want to use a sinon fake that returns a promise which I know works. Mocha tells me the error is that the function is not returning a promise via the error Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. I imagine I have constructed the fake improperly but I cannot see how else I should have done this.
const { expect, use } = require('chai');
const sinon = require('sinon');
const sinonChai = require("sinon-chai");
const utils = require('../utility/exec');
use(sinonChai);
it('sinon fake should resolve', async () =>{
const fake = sinon.fake.resolves('resolved');
const result = await fake();
expect(result).to.equal('resolved');
});
describe('Utility Functions', () =>{
describe('shellExec', () =>{
it('should accept an alternate execute function', async () =>{
const fakeShell = { exec: sinon.fake.resolves('pass') };
const result = await utils.shellExec('pwd', 'xyz', fakeShell);
expect(result).to.equal('pass');
expect(fakeShell.exec).to.have.been.calledOnce;
});
});
});

You function is a little complex but nothing sinon can't handle with stubs. See https://sinonjs.org/releases/v1.17.7/stubs/ for more info but what you should use is callsArgOnWith before the function.
Instead of setting exec to return a promise you need to set it as a stub. This way you can call the callback using the callsArgOnWith function when it is encountered.
I've changed your test so it now passes by changing the fake exec function to return a stub const fakeShell = { exec: sinon.stub() }; and adding the line fakeShell.exec.callsArgOnWith(2, null, null, 'pass', null) before running your function
const { expect, use } = require('chai');
const sinon = require('sinon');
const sinonChai = require("sinon-chai");
const utils = require('./main');
use(sinonChai);
it('sinon fake should resolve', async () =>{
const fake = sinon.fake.resolves('resolved');
const result = await fake();
expect(result).to.equal('resolved');
});
describe('Utility Functions', () =>{
describe('shellExec', () =>{
it('should accept an alternate execute function', async () =>{
const fakeShell = { exec: sinon.stub() };
fakeShell.exec.callsArgOnWith(2, null, null, 'pass', null)
const result = await utils.shellExec('pwd', 'xyz', fakeShell);
expect(result).to.equal('pass');
expect(fakeShell.exec).to.have.been.calledOnce;
});
});
});

your _shell.exec is just a callback function, It's not a Promise. That's why when you fake shell.exec to be a promise, your resolve will never been called. I think you need to fake your fakeShell to something like this:
const fakeShell = {
exec: (cmd, options, cb) => {
cb(true, 'pass', null);
}
};

Related

Why is my resetAllMocks not working in jest

The second expect(fs.writeFile).toHaveBeenCalledTimes(1) (in describe('Guid for MPX') returns an error because the writeFile has been called twice. In theory, jest.ResetAllMocks should take care of this but it doesn’t.
'use strict';
const fs = require('fs').promises;
const path = require('path');
const guidForMpxInvalid = require('../json/guid-for-Mpx-invalid.json')
const data = require('../../../data/sandbox-data-model.json');
jest.mock('fs', () => ({
promises: {
writeFile: jest.fn(),
},
}));
const {
writeData,
createGuidForMpx,
createMpxForGuid,
} = require('../app/transform');
const directoryPath = path.join(__dirname, '../../../wiremock/stubs/mappings');
describe('Write file', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should write a file', async () => {
const result = await writeData(guidForMpxInvalid, 'guid-for-Mpx-invalid-Mpx.json');
expect(result).toEqual('guid-for-Mpx-invalid-Mpx.json written');
expect(fs.writeFile).toHaveBeenCalledTimes(1);
});
});
describe('Guid for MPX', () => {
it('should create JSON file for the GUID of a particular MPX', async ()=>{
const result = await createGuidForMpx(data.Customers[0].guid, data.Customers[0].Customer_Overlays.core.Personal_Details.MPX);
expect(result).toEqual('guid-for-Mpx-AB123456B.json written');
expect(fs.writeFile).toHaveBeenCalledTimes(1);
});
});
The code being called:
const writeData = async (data, file) => {
const directoryPath = path.join(__dirname, '../../wiremock/stubs/mappings');
try {
fs.writeFile(`${directoryPath}/${file}`, data);
return `${file} written`
} catch (err) {
return err;
}
};
I was experiencing the same problem until I placed jest.resetAllMocks(); inside afterEach like so:
afterEach(() => {
jest.resetAllMocks();
});
I eventually got this working by creating a spy for the writefile at the start of each test and clearing it when the test is done:
it('should write a file', async () => {
const writeFileSpy = jest.spyOn(fs, 'writeFile');
const result = await writeData(guidForMPXInvalid, 'guid-for-mpx-invalid-mpx.json');
expect(result).toEqual('guid-for-mpx-invalid-mpx.json written');
expect(writeFileSpy).toHaveBeenCalledTimes(1);
writeFileSpy.mockClear();
});
});
Same thing here. I had to use spyOn as well, which is probably better practice.
All should beware when not using spyOn with complex libraries, double check that your reset works, but safe practice is to manually restore the function you mocked.
There is an issue it seems, perhaps because how fs/promises is included. fs.promises is a Getter function and is lazy loaded from internal/fs/promises, and jest is seemingly unable to clean lazy loaded modules with jest.resetModules?
See related note by #john-james regarding moduleNameMapper:
Jest not working with fs/promises typescript
Another documented error with resetModules():
https://github.com/facebook/jest/issues/11632

Jest: child_process.exec.mockImplentation is not a function

I have a function that uses the child_process.exec function:
//serverUtils.js:
const { promisify } = require('util');
const exec = promisify(require('child_process').exec);
async getUpstreamRepo() {
try {
const forkConfig = (await exec('git remote get-url upstream')).stdout;
let upstreamRepo = forkConfig.replace('git#github.com:', '');
upstreamRepo = upstreamRepo.replace(/\r?\n|\r/g, '');
return upstreamRepo;
} catch (error) {
console.error(error);
throw error;
}
},
After looking at this SO post, I tried to mock the exec call like so:
//serverUtils.test.js:
const child_process = require('child_process');
jest.mock('child_process')
describe('Test Class', () => {
....
it('check upstream repo', async () => {
child_process.exec.mockImplentation(jest.fn().
mockReturnValueOnce('git#github.com:mock/url.git'))
await expect(serverScript.getUpstreamRepo()).
resolves.toEqual('mock/url.git');
});
}
However, I get child_process.exec.mockImplentation is not a function
As the linked post explains, "Jest documentation says that when mocking Node's core modules calling jest.mock('child_process') is required." -- which I clearly do.
The error you are seeing is because you are calling mockImplentation instead of mockImplementation. Unfortunately, when you correct that typo the test still will not pass.
This is because you are calling promisify on exec method, allowing it to be used as a promise. What promisify does under the hood is transform from an asynchronous callback based function (where the callback is placed at last parameter and is called with error as first parameter and data as second) to a promise based function.
So, in order for the promisify method to work, you will have to mock the exec method so that it calls the callback parameter in order for the promise to resolve.
Also, note that you are reading the stdout parameter from the result of the exec call, so in the returned data you will have to send an object with that property.
Having all that into account:
it('check upstream repo', async () => {
child_process.exec.mockImplementation((command, callback) => {
callback(null, { stdout: 'git#github.com:mock/url.git' });
});
await expect(serverScript.getUpstreamRepo()).
resolves.toEqual('mock/url.git');
});
Another posible solution is to directly mock the promisify method:
jest.mock('util', () => ({
promisify: jest.fn(() => {
return jest.fn().mockResolvedValue({ stdout: 'git#github.com:mock/url.git' });
})
}));
describe('Test Class', () => {
it('check upstream repo', async () => {
await expect(serverScript.getUpstreamRepo()).
resolves.toEqual('mock/url.git');
});
});

Mocha times out calling async promise chain in Before hook despite using done

I'm running suite of async integration tests into a mongoose db using mocha and chai on node.js. Most are running fine but for one I have to carry out some pre-test db-prep with the before hook. When I use Done in the before hook Mocha times out.
Error I'm getting is "Error: Timeout of 5000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/Users/donfelipe/Sites/Agents/test/agents.js)"
Have tried moving the done() into a finally block at the end of the promise chain, but that just results in the the it block running before the async chain has finished executing. Kind of stumped.
/*
test case to check that the get function returns
*/
process.env.NODE_ENV = 'test-db';
'use strict'
const mongoose = require("mongoose");
const schemas = require('../app_api/models/schemas');
const agents = require('../app_api/controllers/agents.js');
const lists = require('../app_api/controllers/lists.js');
const server = require('../app.js');
const assert = require('assert')
const config = require('config')
//Require the dev-dependencies
const chai = require('chai');
const chaiHttp = require('chai-http');
const should = chai.should();
chai.use(chaiHttp);
describe('Agents test Suite', () => {
beforeEach(() => {
//don't have anything too do in this case
})
describe('/GET listAddItem suite', function () {
//before we can test deletion we have to add at least one list in.
const userModel = mongoose.model('User', schemas.userSchema);
const agentModel = mongoose.model('Agent', schemas.agentSchema);
let agentToAddId = {}
const listObject = {
listName: 'testList',
_id: mongoose.Types.ObjectId(),
agents: [{
_id: "00000000000000000000000",
name: "Test agent"
}]
}
const lengthToTestAgainst = listObject.agents.length
beforeEach((done) => {
userModel.findById(config.defaultUserId)
.select('agentList')
.then((parentDoc) => {
if (!parentDoc) {
console.error('Unable to find user');
}
parentDoc.agentList.push(listObject)
return parentDoc.save()
})
.then(() => {
return agentModel.find({})
})
.then((response) => {
agentToAddId = response[0]._id
//console.log(response[0]._id);
done()
})
})
it('should add a new item into the testList', (done) => {
chai.request(server)
.get(`/agents_api/listAddItem/${config.defaultUserId}/${listObject._id}/${agentToAddId}`)
.end((err, response) => {
response.should.have.status(200)
response.body.agentList.testList.should.not.be.equal(lengthToTestAgainst + 1)
done(err)
})
})
})
})
Duh. Resolved this myself. A real case of the tail wagging the dog.
So I mocked up the call to the API that the chai.request(server) is making:
/agents_api/listAddItem/${config.defaultUserId}/${listObject._id}/${agentToAddId}
and submitted it through POSTMAN. Turns out there was a bug in the API and so it was not returning a response, so the timeout I was getting from Mocha was a valid response, the request was just hanging.

Mocha complains about done() even though it is used

I am writing some tests of Solidity using Mocha module. The test fails below with this error despite the fact that the done() function is called and the promise is resolved (the commented out console.log() statements show that the Promise from the included module compile.js indeed resolves).
Perhaps I'm not interpreting the error correctly? I am new to Node.js, so my apologies if I cooked up a mess.
"before each" hook for "Deploy a contract":
Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.
const assert = require('assert');
const ganache = require('ganache-cli');
const Web3 = require('web3');
const web3 = new Web3(ganache.provider());
let accounts;
let inbox;
beforeEach(async (done) => {
// Get a list of all accounts
accounts = await web3.eth.getAccounts();
// console.log(accounts);
const generate = require('../compile');
await generate()
.then(async data => {
var interface = data.interface;
var bytecode = data.bytecode;
// console.log('ABI ' + interface);
// console.log('BIN ' + bytecode);
inbox = await new web3.eth.Contract(JSON.parse(interface))
.deploy({data: bytecode, arguments: ['Greetings!']})
.send({from: accounts[0], gas: '1000000'});
});
done();
});
describe('Inbox testing', () => {
it('Deploy a contract', () => {
console.log('Contract ' + inbox);
});
});
The function generate() imported from compile.js returns promise
function generate() {
return new Promise((resolve, reject) => {
...
})
})
}
module.exports = generate;
You cannot use a done callback with an async function in Mocha. Also, it's not a good idea to pass an async function to .then. I would refactor the test function to use asynchronous style code only.
beforeEach(async () => {
// Get a list of all accounts
const accounts = await web3.eth.getAccounts();
// console.log(accounts);
const generate = require('../compile');
const data = await generate();
var interface = data.interface;
var bytecode = data.bytecode;
// console.log('ABI ' + interface);
// console.log('BIN ' + bytecode);
inbox = await new web3.eth.Contract(JSON.parse(interface))
.deploy({data: bytecode, arguments: ['Greetings!']})
.send({from: accounts[0], gas: '1000000'});
});
I think mocha might be going crazy because you need to manually close your web3 connection after running your tests. Try calling disconnect after your tests run:
after(done => {
web3.currentProvider.disconnect()
done();
}

Node.js exporting a function that returns an object

I am trying to export a function I defined in a file (chat.js) and use it in another file (main.js).
This is the code for chat.js
module.exports.list = function() {
var chats;
sequelize.query('SELECT * FROM Chats', {model: Chats}).then((chat) =>{
console.log(chat);
chats = chat
})
return chat;
};
When I use it in main.js
chat.list();
It works as expected.
However I get undefined when I store the return object to a variable and console.log it
You are returning chat object in synchronous way but its asynchronous query processing.
Use promise chaining or async/await.
module.exports.list = function () {
var chats;
return new Promise((resolve, reject) => {
sequelize
.query('SELECT * FROM Chats', {model: Chats})
.then((chat) => {
console.log(chat);
resolve(chat);
})
})
};
somewhere :
function async test(){
let result = await chat.list();
console.log(result);
}

Resources