2

I have a module which instantiates imported classes and asynchronously calls methods of those instances.

How can I mock those methods independently per test case so these mocks only make sense for instance, which is created inside a test case, given that I cannot reliably restore mocks at the end of the test?

Example:

// tested class 
import B from './b';
import C from './c';

export default class A {
  someFunction() {
    let instanceB = new B();
    return instanceB.doSomething()
      .then(() => this.doSomethingElse())
      .then((data) => {
        // async context, other tests will start before this.
        let instanceC = new C(data);
      });
  }
}

// test
import A from './a';
describe('test', () => {
  it('case1', () => {
    a = new A();
    // Mock B, C with config1
    return a.someFunction().then(() => {/* asserts1 */ });
  })
  it('case2', () => {
    a = new A();
    // Mock B, C with config2
    return a.someFunction().then(() => {/* asserts2 */ });
  })
})

If I mock B and C in case1 and restore them synchronously, C's config will be overwritten because case2 runs before C's instantiation in the async context. For the same reason I cannot restore mocks asynchronously after asserts1.

There are similar questions: Stubbing a class method with Sinon.js, How to mock dependency classes for unit testing with mocha.js? but they don't cover the problem of async mocks.

Anatoly Sazanov
  • 1,814
  • 2
  • 14
  • 24

1 Answers1

0

So I ended up with (not pretty) constructor injection. If you have a better way, including maybe a completely different approach to testing and writing async factories, please share, I'll gladly accept that

// tested class 
import B_Import from './b';
import C_Import from './c';

let B = B_Import;
let C = C_Import;
export function mock(B_Mock, C_Mock) {
    B = B_Mock || B_Import;
    C = C_Mock || C_Import;
}

export default class A {
  someFunction() {
    let instanceB = new B();
    return instanceB.doSomething()
      .then(() => this.doSomethingElse())
      .then((data) => {
        // async context, other tests will start before this.
        let instanceC = new C(data);
      });
  }
}


// test
import A, { mock as mockB } from './a';

setupMockB = (cfg, mockCallback) =>  {
    const ClassMock = class {
        constructor() {
            // use cfg
        }
        doSomething() {}
    } 
    if(mockCallback) {mockCallback(ClassMock.prototype);}
    mockB(ClassMock, null)
}

describe('test', () => {
  afterEach(() => mockB())
  it('case1', () => {
    a = new A();
    setupMockB(cfg1, (mb) => sinon.stub(mb, 'doSomething').resolves())
    return a.someFunction().then(() => { /* asserts1 */ });
  })
  it('case2', () => {
    a = new A();
    setupMockB(cfg2, (mb) => sinon.stub(mb, 'doSomething').rejects())
    return a.someFunction().then(() => { /* asserts2 */ });
  })
})
Anatoly Sazanov
  • 1,814
  • 2
  • 14
  • 24