node.jspromisemocha.jsshelljs

Unit test with sinon fake does not resolve promise


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;
        });
    });
});

Solution

  • 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;
            });
        });
    });