Search code examples
javascripttestingsinonproxyquire

Stubs and proxquire


I'm having an issue with stubbing a particular function with sinon after having used proxyquire.

Example:

// a.js
const api = require('api');

module.exports = (function () {
    return {
        run,
        doStuff
    };

    function run() {
        return api()
            .then((data) => {
                return doStuff(data);
            })
    }

    function doStuff(data) {
        return `Got data: ${data}`;
    }
})()

// a.spec.js - in the test
a = proxyquire('./a', {
    'api': () => Promise.resolve('data')
})
sinon.stub(a, 'doStuff');
// RUN TEST - call a.run()

I know it isn't working because it calls the original doStuff instead of a mocked/stubbed doStuff.


Solution

  • I know it isn't working because it calls the original doStuff instead of a mocked/stubbed doStuff.

    That is because function run() in a.js invokes function doStuff(data) inside the closure function run() is holding over the content of a.js and not function doStuff(data) in a_spec.js.

    Illustration by example

    Lets re-write a.js to:

    var a = 'I am exported';
    var b = 'I am not exported';
    
    function foo () {
        console.log(a);
        console.log(this.b)
    }
    
    module.exports.a=a;
    module.exports.foo=foo;
    

    and a.spec.js to:

    var one = require('./one');
    
    console.log(one); // { a: 'I am exported', foo: [Function: foo] }
    
    one.a = 'Charles';
    one.b = 'Diana';
    
    console.log(one); // { a: 'Charles', foo: [Function: foo], b: 'Diana' }
    

    Now if we invoke one.foo() it will result in:

    I am exported
    Diana
    

    The I am exported is logged to the console because console.log(a) inside foo points to var a inside the closure foo is holding over the contents of a.js.

    The Diana is logged to the console because console.log(this.b) inside foo points to one.b in a.spec.js.

    So what do you need to do to make it work?

    You need to change:

    module.exports = (function () {
        return {
            run,
            doStuff
        };
    
        function run() {
            return api()
                .then((data) => {
                    return doStuff(data);
                })
        }
    
        function doStuff(data) {
            return `Got data: ${data}`;
        }
    })()
    

    to:

    module.exports = (function () {
        return {
            run,
            doStuff
        };
    
        function run() {
            return api()
                .then((data) => {
                    return this.doStuff(data); // ´this.doStuff´ points to ´doStuff´ in the exported object
                }.bind(this)) // to ensure that ´this´ does not point to the global object
        }
    
        function doStuff(data) {
            return `Got data: ${data}`;
        }
    })()