Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to properly mock ES6 classes with sinon

I want to be able to properly test my ES6 class, it's constructor requires another class and all this looks like this:

Class A

class A {
  constructor(b) {
    this.b = b;
  }

  doSomething(id) {
    return new Promise( (resolve, reject) => {
      this.b.doOther()
        .then( () => {
          // various things that will resolve or reject
        });
    });
  }
}
module.exports = A;

Class B

class B {
  constructor() {}

  doOther() {
    return new Promise( (resolve, reject) => {
      // various things that will resolve or reject
    });
}
module.exports = new B();

index

const A = require('A');
const b = require('b');

const a = new A(b);
a.doSomething(123)
  .then(() => {
    // things
  });

Since I'm trying to do dependency injection rather than having requires at the top of the classes, I'm not sure how to go about mocking class B and it's functions for testing class A.

like image 696
Jarede Avatar asked May 17 '18 11:05

Jarede


3 Answers

Sinon allows you to easily stub individual instance methods of objects. Of course, since b is a singleton, you'll need to roll this back after every test, along with any other changes you might make to b. If you don't, call counts and other state will leak from one test into another. If this kind of global state is handled poorly, your suite can become a hellish tangle of tests depending on other tests.

Reorder some tests? Something fails that didn't before. Add, change or delete a test? A bunch of other tests now fail. Try to run a single test or subset of tests? They might fail now. Or worse, they pass in isolation when you write or edit them, but fail when the whole suite runs.

Trust me, it sucks.

So, following this advice, your tests can look something like the following:

const sinon = require('sinon');
const { expect } = require('chai');
const A = require('./a');
const b = require('./b');

describe('A', function() {
    describe('#doSomething', function() {
        beforeEach(function() {
            sinon.stub(b, 'doSomething').resolves();
        });

        afterEach(function() {
            b.doSomething.restore();
        });

        it('does something', function() {
            let a = new A(b);

            return a.doSomething()
                .then(() => {
                    sinon.assert.calledOnce(b.doSomething);
                    // Whatever other assertions you might want...
                });
        });
    });
});

However, this isn't quite what I would recommend.

I usually try to avoid dogmatic advice, but this would be one of the few exceptions. If you're doing unit testing, TDD, or BDD, you should generally avoid singletons. They do not mix well with these practices because they make cleanup after tests much more difficult. It's pretty trivial in the example above, but as the B class has more and more functionality added to it, the cleanup becomes more and more burdensome and prone to mistakes.

So what do you do instead? Have your B module export the B class. If you want to keep your DI pattern and avoid requiring the B module in the A module, you'll just need to create a new B instance every time you make an A instance.

Following this advice, your tests could look something like this:

const sinon = require('sinon');
const { expect } = require('chai');
const A = require('./a');
const B = require('./b');

describe('A', function() {
    describe('#doSomething', function() {
        it('does something', function() {
            let b = new B();
            let a = new A(b);
            sinon.stub(b, 'doSomething').resolves();

            return a.doSomething()
                .then(() => {
                    sinon.assert.calledOnce(b.doSomething);
                    // Whatever other assertions you might want...
                });
        });
    });
});

You'll note that, because the B instance is recreated every time, there's no longer any need to restore the stubbed doSomething method.

Sinon also has a neat utility function called createStubInstance which allows you to avoid invoking the B constructor completely during your tests. It basically just creates an empty object with stubs in place for any prototype methods:

const sinon = require('sinon');
const { expect } = require('chai');
const A = require('./a');
const B = require('./b');

describe('A', function() {
    describe('#doSomething', function() {
        it('does something', function() {
            let b = sinon.createStubInstance(B);
            let a = new A(b);
            b.doSomething.resolves();

            return a.doSomething()
                .then(() => {
                    sinon.assert.calledOnce(b.doSomething);
                    // Whatever other assertions you might want...
                });
        });
    });
});

Finally, one last bit of advice that's not directly related to the question-- the Promise constructor should never be used to wrap promises. Doing so is redundant and confusing, and defeats the purpose of promises which is to make async code easier to write.

The Promise.prototype.then method comes with helpful behavior built-in so you should never have to perform this redundant wrapping. Invoking then always returns a promise (which I will hereafter call the 'chained promise') whose state will depend on the handlers:

  • A then handler which returns a non-promise value will cause the chained promise to resolve with that value.
  • A then handler which throws will cause the chained promise to reject with the thrown value.
  • A then handler which returns a promise will cause the chained promise to match the state of that returned promise. So if it resolves or rejects with a value, the chained promise will resolve or reject with the same value.

So your A class can be greatly simplified like so:

class A {
  constructor(b) {
    this.b = b;
  }

  doSomething(id) {
      return this.b.doOther()
        .then(() =>{
          // various things that will return or throw
        });
  }
}
module.exports = A;
like image 124
sripberger Avatar answered Oct 16 '22 22:10

sripberger


I think you're searching for the proxyquire library.

To demonstrate this, I edited a little bit your files to directly include b in a (I did this because of your singleton new B), but you can keep your code, it's just more easy to understand proxyquire with this.

b.js

class B {
  constructor() {}
  doOther(number) {
    return new Promise(resolve => resolve(`B${number}`));
  }
}

module.exports = new B();

a.js

const b = require('./b');

class A {
  testThis(number) {
    return b.doOther(number)
      .then(result => `res for ${number} is ${result}`);
  }
}

module.exports = A;

I want now to test a.js by mocking the behavior of b. Here you can do this:

const proxyquire = require('proxyquire');
const expect = require('chai').expect;

describe('Test A', () => {
  it('should resolve with B', async() => { // Use `chai-as-promised` for Promise like tests
    const bMock = {
      doOther: (num) => {
        expect(num).to.equal(123);
        return Promise.resolve('__PROXYQUIRE_HEY__')
      }
    };
    const A = proxyquire('./a', { './b': bMock });

    const instance = new A();
    const output = await instance.testThis(123);
    expect(output).to.equal('res for 123 is __PROXYQUIRE_HEY__');
  });
});

Using proxyquire you can easily mock a dependency's dependency and do expectations on the mocked lib. sinon is used to directly spy / stub an object, you have to use generally both of them.

like image 4
Patrick Portal Avatar answered Oct 16 '22 22:10

Patrick Portal


Seems pretty straightforward, since sinon mocks an object by replacing one of its methods with a behavior (as described here):

(I added resolve()-s to both of the promises in your functions to be able to test)

const sinon = require('sinon');

const A = require('./A');
const b = require('./b');

describe('Test A using B', () => {
  it('should verify B.doOther', async () => {
    const mockB = sinon.mock(b);
    mockB.expects("doOther").once().returns(Promise.resolve());

    const a = new A(b);
    return a.doSomething(123)
      .then(() => {
        // things
        mockB.verify();
      });
  });
});

Please let me know if I misunderstood something or additional detail what you'd like to test...

like image 1
Gyuri Avatar answered Oct 17 '22 00:10

Gyuri