0

Simplified problem case:

export class MyClass {

    constructor() {
        this.myMethod();
    }

    myMethod() {
        console.log(42);
    }

}

Testing the constructor:

describe('CLASS: MyClass', () => {
    let sut: MyClass;

    beforeEach(() => {
        jest.clearAllMocks();
        sut = new MyClass();
    });

    describe('CONSTRUCTOR', () => {
    
        test('should construct correctly and call myMethod', () => {
            const spy = jest.spyOn(sut, 'myMethod').mockImplementationOnce(jest.fn());
    
            expect(sut).toBeTruthy();
            expect(spy).toHaveBeenCalled();
        });    
    });
});

Of course this doesn't work, as the spy is initiated after sut is constructed, so it can't register the call.

Neither is it possible to initiate the spy before sut, as it can't spy on something that doesn't exist yet.

Nor did I have success trying to spy on MyClass.prototype.

Sure, I could spy on the implementation details of myMethod (basically jest.spyOn(console, 'log'). But that defies the separation of units for testing.

It's probably trivial, but what am I missing, how to get this very simple test to work?

Anders Bernard
  • 541
  • 1
  • 6
  • 19
  • 1
    Don't mock parts of the thing you're supposed to be testing. Test _behaviour_; if that method really does things that aren't this class's responsibility, then extract it to a collaborator. See also: https://stackoverflow.com/a/66752334/3001761 – jonrsharpe Nov 23 '22 at 07:40
  • @jonrsharpe You are absolutely right, though the code I'm testing isn't mine but a customers. And while I've got the thumbs up for some (heavily needed) refactoring, there is only so much I can change the implementation without getting outside of the scope of my work assignment. So what you suggest might not always be applicable for non-IT reasons. ;) In this specific case, the method was doing nothing but point to a collaborator, so it was actually redundant. But I can't simply remove it, as the service in question follows a style that requires said method. And we can't have THAT. ;) – Anders Bernard Nov 23 '22 at 07:57

1 Answers1

1

You should spy on MyClass instead of the instantiated class, sut.

You can still spy on methods without mocking the implementation but it's probably better here to avoid the code in it getting executed and logging 42 in the console.

describe('CLASS: MyClass', () => {
  let sut: MyClass
  let spy: jest.SpyInstance = jest.spyOn(MyClass.prototype, 'myMethod')

  beforeEach(() => {
    jest.clearAllMocks()
    spy.mockImplementationOnce(jest.fn())
    sut = new MyClass()
  })

  describe('CONSTRUCTOR', () => {
    test('should construct correctly and call myMethod', () => {
      expect(sut).toBeTruthy()
      expect(spy).toHaveBeenCalled()
      expect(spy).toHaveBeenCalledTimes(1)
    })
  })
})
josephting
  • 2,617
  • 2
  • 30
  • 36
  • Works like a charme. So I was right when I tried using the prototype, but apparently mixed up the syntax somehow. Thank you. :D – Anders Bernard Nov 23 '22 at 07:40
  • 1
    To make sure people don't miss the caveat with this answer: please read jonsharpe s comment on the OP, and my answer to it. They examplify, why this approach should only be used sparingly. – Anders Bernard Nov 23 '22 at 08:05