javascriptnode.jsunit-testingsinonstub

sinon.restore in one file restores any top level stubs in another file


Service1:

const { Service2 } = require('../Service2.js');

const func1 = () => {
  const result = Service2.func2();
  return result;
}

const Service1 = { func1 };
module.exports = { Service1 };

Test for Service1:

Note that I need to test Service1.func1 which internally depends on Service2.func2, hence i would like to stub Service2.func2

const sinon = require('sinon');
const { expect } = require('chai');
const { Service2 } = require('../Service2');

const func2stub = sinon.stub(Service2, 'func2');

// import Service1 after stubbing Service2.func2, so that it uses the stubbed version of func2
const { Service1 } = require('../Service1');

describe('Service1 Tests', () => {
  it('should return some result on calling func1', () => {
    func2stub.returns(true);
    expect(Service1.func1()).to.equal(true);
  }
}

This works fine on its own. But If I have another unrelated test file which calls sinon.restore() in its lifecycle, my Service1 tests start calling the actual implementation of Service2.func2 instead of the stub.

// Another test file

...
...
describe('some other tests', () => {
  afterEach(() => { sinon.restore(); }
  it('should ...', () => {
    ...
  }
}

...

I have debugged this by commenting out the sinon.restore , and by running just the Service1 tests in isolation.

During my debugging, a workaround that I found for this is to move the stubbing from the top level to before lifecycle in the Service1 tests

const sinon = require('sinon');
const { expect } = require('chai');

let Service1;
let func2stub;

describe('Service1 Tests', () => {
  before(() => {
   const { Service2 } = require('../Service2');
   func2stub = sinon.stub(Service2, 'func2');
   // import Service1 after stubbing Service2.func2, so that it uses the stubbed version of func2
   Service1 = require('../Service1').Service1;
  }

  it('should return some result on calling func1', () => {
    func2stub.returns(true);
    expect(Service1.func1()).to.equal(true);
  }
}

This works fine, and is not impacted by the sinon.restore in the other file.

Any pointers to understand why this happens and the proper solution would be helpful.


Solution

  • sinon provides a default sandbox global to the module, which means everything in the default sandbox is restored when sinon.restore() is called.

    It's possible to use sinon.createSandbox() to create a more granular sandbox relevant to the service, or test suite, or at whatever level you want to restore.

    describe('tests', function(){
      
      let sandbox;
    
      before(function(){
        sandbox = sinon.createSandbox({})
      })
    
      afterEach(function(){
        sandbox.restore()
      })
    
      it('should test', function(){
        sandbox.spy(x)
      })
    
    }
    

    mocha has two stages to a run

    1. Setting up the tests to run
    2. Running the tests

    Anything outside of a before/after/it runs immediately during the setup phase.

    Read test js files to build test run.
    Traverse `describe` functions, running the attached callback function. 
    Schedule `it` `before` `after` functions for test run. 
    - Read Blah tests
    - Read Service1 tests < Service2 stub setup here
    - Read Service2 tests
    
    Execute test run
    - Run Blah tests  < sinon.restore() removes service2 stub.
    - Run Service1 tests
    - Run Service2 tests 
    

    So mocha has a complete picture of what tests can run, before they run.