5

Consider two classes A and B like this:

class A {
    private b: B;

    public constructor(b: B){
        this.b=b;
    }

    public doSomething(){
        this.b.myMethod();
    }
}

class B {
    public myMethod(){...}
    public someOtherMethod(){...}
}

I would like to Test class A while mocking the behaviour of B.myMethod()

Currently we do this like this:

const bMock: Partial<B> = {
    myMethod: jest.fn(<some mock here>),
}

const sut = new A(bMock as any);

sut.doSomething();

expect(bMock.myMethod).toBeCalled();

What we would like to achieve is a similar result but without having to pass the mock with as any and without having to mock all methods on our own. Having the mock type checked is very important for us as otherwise we will not catch breaking changes in the mocked dependencies with this test.

We already had a look into sinon as well, but in some cases we do not want the constructor of our mocked dependency to be invoked and thus stubbing the object after creation is not an option. Stubbing the whole class causes similar issues like described above.

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Abaddon666
  • 1,533
  • 15
  • 31

2 Answers2

2

EDIT: Nowadays we are using jest-mock-extended for better jest compatibility and more features

I found a nice solution by using Substitute.

Only Issue being the missing checks for null/undefined as mentioned in the README. Still better than using as any though.

import Substitute, { SubstituteOf } from '@fluffy-spoon/substitute';

class A {
  private b: B;

  public constructor(b: B) {
    this.b = b;
  }

  public doSomething() {
    return this.b.myMethod();
  }
}

class B {
  public myMethod() {
    return 'realMethod';
  }
  public someOtherMethod() {
    return 'realSomeOtherMethod';
  }
}

let bMock: SubstituteOf<B>;
beforeEach(() => {
  bMock = Substitute.for<B>();
});

test('empty mock', () => {
  const sut = new A(bMock);
  console.log(sut.doSomething()); // Output: '[Function]'
});

test('mock myMethod', () => {
  bMock.myMethod().returns('You got mocked!');
  const sut = new A(bMock);
  console.log(sut.doSomething()); // Output:  'You got mocked!'
});

test('does not compile', () => {
  bMock.myMethod().returns(1337); // Will show compilation error due to incompatible type (string vs. number)
  const sut = new A(bMock);
  console.log(sut.doSomething());
});

test('missing null checks', () => {
  bMock.myMethod().returns(); // Will not complain
  const sut = new A(bMock);
  console.log(sut.doSomething()); // Output 'undefined'
});
Abaddon666
  • 1,533
  • 15
  • 31
0

What about creating a generic? (you can use Partial as it's in your example but you have to check if you've provided the 'sut.doSomething' implementation before calling it.

class B {
  public myMethod(){}
  public someOtherMethod(){}
}

class A<T extends B> {
  private b: T;

  public constructor(b: T){
      this.b=b;
  }

  public doSomething(){
      this.b.myMethod();
  }
}

const bMock = {myMethod: jest.fn(), someOtherMethod: jest.fn()}
const sut = new A(bMock)
sut.doSomething()
expect(bMock.myMethod).toBeCalled();
Lajos Gallay
  • 1,169
  • 7
  • 16
  • Thank you for your Answer. Unfortunately, this does not help me. Using a generic `` will still require `T` to follow the whole interface of B (what your bMock actually does). I would like to be able to pass `bMock` without having to worry about `someOtherMethod()` and also without losing type safety. So basically a way to replace all (public) members, including the constructor, with a mock fitting its type and only add a mock implementation for specific, individual methods. – Abaddon666 Jun 13 '19 at 11:43
  • I see. The best you can do that you can replace "A" to "Avoid}>" and list all methods that class A uses there (or maybe extract to a separate interface) but that will duplicate the work - or you can cast somewhere to 'any' - that you did in your question. – Lajos Gallay Jun 13 '19 at 11:55
  • The second part of https://stackoverflow.com/a/63074930/1402988 shows only mocking a single method from the class safely in TypeScript with Jest. – Piran Jul 24 '20 at 13:58