最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

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

programmeradmin2浏览0评论

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: (Simply npm install && npm test, watch the test fail.) This question seeks a solution make the test pass without changing main code.

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./markni/Stackoverflow30825202 (Simply npm install && npm test, watch the test fail.) This question seeks a solution make the test pass without changing main code.

Share Improve this question edited Jun 18, 2015 at 4:48 Mark Ni asked Jun 14, 2015 at 1:35 Mark NiMark Ni 2,4011 gold badge28 silver badges34 bronze badges 2
  • 1 Each test should be able to run in isolation so stubbing it before each test is appropriate and correct. Depending on earlier tests stubbing (changing state) is a very bad idea. – Cymen Commented Jun 14, 2015 at 1:43
  • Your use of .yield() suggests that .doWork() is async, but the rest of your code (inclusing your test) doesn't. – robertklep Commented Jun 14, 2015 at 7:05
Add a ment  | 

3 Answers 3

Reset to default 2 +50

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 e 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 plicated 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.

发布评论

评论列表(0)

  1. 暂无评论