4

I have 2 test cases which test the same function just taking 2 different executions paths, so to illustrate:

MyClass.prototype.functionBeingTested = function() {
    if (this.check1()) {
        this.isCheck1Called = true;
    } else if (this.check2()) {
        this.isCheck1Called = false;
    } else {
        ...
    }
};

My 2 test cases are as follow:

it('should take check1() execution path', function() {
    var myClass= new MyClass({}, {}, {});
    var check1Stub sinon.stub(MyClass.prototype, 'check1');
    check1Stub.returns(true);
    myClass.functionBeingTested();
    myClass.isCheck1Called.should.equal(true);
});

it('should take check2() execution path', function() {
    var myClass= new MyClass({}, {}, {});
    var check2Stub sinon.stub(MyClass.prototype, 'check2');
    check2Stub.returns(true);
    myClass.functionBeingTested();
    myClass.isCheck1Called.should.equal(false);
});

Now by default, check1() returns false so I don't stub it in the second test case, but by the time the second case is running, the check1() function stub is still active and causes the second case to enter the execution path of the first case as-well, making the second case test fail.

I understand it's a problem of test running in parallel and the first sinon stub still being used by the first test case, is there anyway I can solve this problem?

Jorayen
  • 1,737
  • 2
  • 21
  • 52

2 Answers2

3

At the end of the first test, you should restore the original method (which is always a good thing, to prevent tests from being influenced by previous tests):

check1Stub.restore()

Or, alternatively, you can use a Sinon sandbox to run each test in:

describe('MyClass', function() {

  beforeEach(function() {
    this.sinon = sinon.sandbox.create();
  });

  afterEach(function() {
    this.sinon.restore();
  });

  it('should take check1() execution path', function() {
      var myClass    = new MyClass({}, {}, {});
      // `this.sinon` is the sandbox
      var check1Stub = this.sinon.stub(MyClass.prototype, 'check1');
      check1Stub.returns(true);
      myClass.functionBeingTested();
      myClass.isCheck1Called.should.equal(true);
  });

  it('should take check2() execution path', function() {
      var myClass    = new MyClass({}, {}, {});
      var check2Stub = this.sinon.stub(MyClass.prototype, 'check2');
      check2Stub.returns(true);
      myClass.functionBeingTested();
      myClass.isCheck1Called.should.equal(false);
  });
});

(See mocha-sinon, which does exactly the same)

robertklep
  • 198,204
  • 35
  • 394
  • 381
  • Awesome, thanks for showing me mocha-sinon alternative, very nice solution – Jorayen May 27 '16 at 11:48
  • I kind of having a problem using it. I made a spec_helper.js file with just `require('mocha-sinon');` in it, and I require it in my test file where I use spy and stubs and it still fails, I see the documentation and the example shown match what i did. Could you update your answer to include the set-up based on what I have ? – Jorayen May 27 '16 at 12:42
  • Such a setup works fine for me, just make sure that you use `this.sinon` instead of `sinon` in your test cases. It _may_ depend on where you call `require('./spec_helper')`, just move that to somewhere outside of the `describe()` block. – robertklep May 27 '16 at 12:52
  • Tests can take different time to finish and this answer does not explain how Sinon stubs/spies are not influencing other parallel tests. You basically say ["use sandbox"](https://sinonjs.org/releases/v15/sandbox/) but don't explain what is it or why would it be helpful ([it doesn't guarantee it](https://stackoverflow.com/a/56257654/104380)) – vsync Apr 24 '23 at 07:18
  • @vsync even though OP was talking about running the tests in parallel, I don't think they were (which is also suggested by my answer solving their issue). – robertklep Apr 24 '23 at 09:08
  • Yeah, but OP is one person, and what about the other thousands who came here from Google because of the question's title? I am on a quest to figure out how to solve multiple tests tripping over each other, wrapping the same methods before the previous ones finished and restored. This is a serious problem.. There are many [questions](https://stackoverflow.com/q/36925936/104380) but no good answers.. – vsync Apr 24 '23 at 09:15
  • @vsync my answer is not based on the title of the question but on the actual issue. If you have an issue with the title you should contact the OP. – robertklep Apr 24 '23 at 10:11
  • title & description match and correct. The question is about running **parallel** tests which are both mocking the same methods. Sandbox does not solve this. Also writing `this.sinon` is not recommended. Also, the syntax is `sinon.createSandbox()` and not `sinon.sandbox.create()`. This answer certainly does not fix the problem when dealing with parallel or async tests. – vsync Apr 24 '23 at 11:25
  • @vsync the answer was marked as solution by OP, so I assume it solved their problem. Also, the answer was written in 2016, with the then-current versions of Mocha and Sinon. – robertklep Apr 24 '23 at 12:32
  • I understand, I am simply saying the marking-as-answered might not be correct and also the versions of Mocha/Sinon are irrelevant because the problem is about **how** the code is written and not about the tools. This problem is highly complex. I am preparing an exact test-case to demo it. – vsync Apr 24 '23 at 12:51
  • @vsync tests in the same file will not be run in parallel by Mocha, parallelisation only happens across test _files_, and each test file is run in a separate process (so I don't see how mocking a method in test file A can cause issues for tests in test file B). – robertklep Apr 24 '23 at 13:53
  • Don't you want to edit the answer in order to update it to modern Sinon syntax? `sandbox.create` was [deprecated in 2019](https://github.com/sinonjs/sinon/blob/HEAD/CHANGES.md#800) – vsync Apr 24 '23 at 16:39
  • @vsync feel free to edit my answer, or place your own answer. I'm not going to start retroactively updating my more than 3000 answers. – robertklep Apr 24 '23 at 18:33
0

Avoid re-wrapping the same method, and lift the common Sinon wrapper to the parent describe method:

import {expect} from 'chai'
import sinon from 'sinon'
import * as common from './common.js'
import foo from './foo.js'

describe('foo', () => {
    let myStub;

    // the stub wrapper is applied once and is available 
    // for all tests in this (describe) block 
    before(() => {
        myStub = sinon.stub(common, 'randomBoolean'); 
    })

    // after all tests (in the describe block) has finished, restore the original method
    after(() => {
        myStub.restore()
    })

    it('should equal 1', async () => {
        myStub.returns(true) // work with the pre-existing stub
        const result = await foo()

        expect(result).to.equal(1)
    })

    it('should equal 2', async () => {
        myStub.returns(false) // work with the pre-existing stub
        const result = await foo()

        expect(result).to.equal(2)
    })
})

When all tests are running in parallel, even if they are async (unknown when each will finish) they still pass and won't conflict.


If you have multiple stubs you easily restore them all using a sandbox:

describe('foo', () => {
    const stubs = {}
    const sandbox = sinon.createSandbox()

    // the stub wrapper is applied once and is available 
    // for all tests in this (describe) block 
    before(() => {
        stubs.a = sandbox.stub(common, 'a'); 
        stubs.b = sandbox.stub(common, 'b'); 
        stubs.c = sandbox.stub(common, 'c').returns('whatever'); 
    })

    // after all tests (in the describe block) has finished, restore the original method
    after(() => {
        sandbox.restore()
    })

    it('should equal 1', async () => {
        stubs.a.returns(true) 
        stubs.b.returns(true)

        // assume "foo" is internally using all 3 stubbed methods
        const result = await foo()

        expect(result).to.equal(1)
    })
}
vsync
  • 118,978
  • 58
  • 307
  • 400