简体   繁体   中英

How stub a global dependency's new instance method in nodejs with sinon.js

Sorry for the confusing title, I have no idea how to better describe it. Let's see the code:

var client = require('some-external-lib').createClient('config string');

//constructor
function MyClass(){
}

MyClass.prototype.doSomething = function(a,b){
  client.doWork(a+b);
}

MyClass.prototype.doSomethingElse = function(c,d){
  client.doWork(c*d);
}

module.exports = new MyClass();

Test:

var sinon = require('sinon');
var MyClass = requre('./myclass');
var client = require('some-external-lib').createClient('config string');

describe('doSomething method', function() {
   it('should call client.doWork()',function(){
      var stub = sinon.stub(client,'doWork');
      MyClass.doSomething();
      assert(stub.calledOnce); //not working! returns false
   })
})

I could get it working if .createClient('xxx') is called inside each method instead, where I stub client with:

var client = require('some-external-lib');

sinon.stub(client, 'createClient').returns({doWork:function(){})

But it feels wrong to init the client everytime the method each being called.

Is there a better way to unit test code above?


NEW : I have created a minimal working demo to demonstrate what I mean: https://github.com/markni/Stackoverflow30825202 (Simply npm install && npm test , watch the test fail.) This question seeks a solution make the test pass without changing main code.

The problem arises at the place of test definition. The fact is that in Node.js it is rather difficult to do a dependency injection. While researching it in regard of your answer I came across an interesting article where DI is implemented via a custom loadmodule function. It is a rather sophisticated solution, but maybe eventually you will come to it so I think it is worth mentioning. Besides DI it gives a benefit of access to private variables and functions of the tested module.

To solve the direct problem described in your question you can stub the client creation method of the some-external-lib module.

var sinon = require('sinon');
//instantiate some-external-lib
var client = require('some-external-lib');

//stub the function of the client to create a mocked client
sinon.stub(client, 'createClient').returns({doWork:function(){})

//due to singleton nature of modules `require('some-external-lib')` inside
//myClass module will get the same client that you have already stubbed
var MyClass = require('./myclass');//inside this your stubbed version of createClient 
//will be called. 
//It will return a mock instead of a real client

However, if your test gets more complicated and the mocked client gets a state you will have to manually take care of resetting the state between different unit tests. Your tests should be independent of the order they are launched in. That is the most important reason to reset everything in beforeEach section

You can use beforeEach() and afterEach() hooks to stub global dependency.

var sinon = require('sinon');
var MyClass = requre('./myclass');
var client = require('some-external-lib').createClient('config string');

describe('doSomething method', function() {
    beforeEach(function () {
        // Init global scope here
        sandbox = sinon.sandbox.create();           
    });


   it('should call client.doWork()',function(){
      var stub = sinon.stub(client,'doWork').yield();
      MyClass.doSomething();
      assert(stub.calledOnce); //not working! returns false
   })
   afterEach(function () {
        // Clean up global scope here
        sandbox.restore();
    });
})

Part of the problem is here: var stub = sinon.stub(client,'doWork').yield();

yield doesn't return a stub. In addition, yield expects the stub to already have been called with a callback argument.

Otherwise, I think you're 95% of the way there. Instead of re-initializing for every test, you could simply remove the stub:

describe('doSomething method', function() {
   it('should call client.doWork()',function(){
      var stub = sinon.stub(client,'doWork');
      MyClass.doSomething();
      assert(stub.calledOnce);
      stub.restore();
   })
})

BTW, another poster suggested using Sinon sandboxes, which is a convenient way to automatically remove stubs.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM