Search code examples
typescriptunit-testingjestjsts-jest

Mock Entire Class including constructor, instance and static functions


I have yet to see any full answer/example for this. In my TypeScript project, I have a Class that has a constructor, a function, and a static function. All three are invoked in the code under test. I'd like to write a unit test using Jest that allows me to check the mocked constructor, mocked function, and mocked static function.

I typically use the mock factory approach for mocking Classes (like below); however, I cannot seem to get this to work.

// ClassA.ts
export class ClassA {
  constructor() {
    // construction logic
  }

  public async func1(x: int): Promise<void> {
    // func1 logic
  }

  static func2(x: string | number): x is string {
    // func2 logic
    let conditionIsMet: boolean;

    // conditionIsMet logic

    if (conditionIsMet) {
      return true;
    }

    return false;
  }
}

// CodeUnderTest.ts
import { ClassA } from './ClassA';

export class ClassB {
  public async foo() {
    if (ClassA.func2('asdf')) {
      const a = new ClassA();
      await a.func1(45);
    }
  }
}

// UnitTest.ts
import { ClassB } from './ClassB';

// mock factory
const mockFunc1 = jest.fn();
const mockStaticFunc2 = jest.fn();
jest.mock('./ClassA', () => ({
  ClassA: jest.fn().mockImplementation(() => ({
    func1: mockFunc1,
    func2: mockStaticFunc2, // <= this doesn't work
  }),
});

beforeAll(() => {
  mockStaticFunc2.mockReturnValue(true);
});
describe('when ClassB.foo() is called', () => {
  describe('when ClassA.func2 is given a string parameter', () => {
    it('then ClassA.func2 is invoked with expected parameters, ClassA is constructed, ClassA.func1 is invoked with expected parameters', () => {
      // arrange
      const clsB = new ClassB();

      // execute
      clsB.foo();

      // expect
      expect(mockStaticFunc2).toHaveBeenCalledTimes(1);
      expect(mockStaticFunc2).toHaveBeenNthCalledWith(1, 'asdf');
      expect(ClassA).toHaveBeenCalledTimes(1);  // constructor was mocked
      expect(mockFunc1).toHaveBeenCalledTimes(1);
      expect(mockFunc1).toHaveBeenNthCalledWith(1, 45);
    });
  });
});

Are there any complete examples that illustrates how I can propertly mock all three (ClassA construtor, ClassA.func1 instance function, and ClassA.func2 static function)?


Solution

  • You didn't mock the static method func2 correctly. The static methods are accessed on the class itself. Besides, take a look es6-class-mocks#automatic-mock

    Here is an example:

    ClassA.ts:

    export class ClassA {
      constructor() {}
      public async func1(x: number): Promise<string> {
        return 'real value';
      }
      static func2(x: string | number) {
        return false;
      }
    }
    

    ClassB.ts:

    import { ClassA } from './ClassA';
    
    export class ClassB {
      public async foo() {
        if (ClassA.func2('asdf')) {
          const a = new ClassA();
          const r = await a.func1(45);
          console.log("🚀 ~ file: ClassB.ts:8 ~ ClassB ~ foo ~ r:", r)
        }
      }
    }
    

    ClassB.test.ts:

    import { ClassB } from './ClassB';
    import { ClassA } from './ClassA';
    
    // Mock each methods manually
    // jest.mock('./ClassA', () => {
    //   const MockClassA = jest.fn(function () {
    //     this.func1 = jest.fn()
    //   });
    //   (MockClassA as any).func2 = jest.fn();
    //   return {
    //     ClassA: MockClassA,
    //   };
    // });
    
    // or, let jest create mocks for you
    jest.mock('./ClassA');
    
    describe('when ClassB.foo() is called', () => {
      describe('when ClassA.func2 is given a string parameter', () => {
        it('then ClassA.func2 is invoked with expected parameters, ClassA is constructed, ClassA.func1 is invoked with expected parameters', async () => {
          expect(jest.isMockFunction(ClassA)).toBe(true);
          expect(jest.isMockFunction(ClassA.func2)).toBe(true);
          expect(jest.isMockFunction(ClassA.prototype.func1)).toBe(true);
          const ClassAMocked = jest.mocked(ClassA);
    
          // arrange
          const clsB = new ClassB();
          jest.mocked(ClassA.func2).mockReturnValueOnce(true);
          jest.mocked(ClassA.prototype.func1).mockResolvedValueOnce('fake value');
    
          // execute
          await clsB.foo();
    
          // expect
          const classAInstance = ClassAMocked.mock.instances[0];
          expect(jest.isMockFunction(classAInstance.func1)).toBe(true);
          expect(ClassAMocked.func2).toHaveBeenNthCalledWith(1, 'asdf');
          expect(ClassAMocked).toHaveBeenCalledTimes(1);
          expect(classAInstance.func1).toHaveBeenNthCalledWith(1, 45);
        });
      });
    });
    

    Test result:

      console.log
        🚀 ~ file: ClassB.ts:8 ~ ClassB ~ foo ~ r: fake value
    
          at ClassB.log (stackoverflow/77559902/ClassB.ts:8:15)
    
     PASS  stackoverflow/77559902/ClassB.test.ts
      when ClassB.foo() is called
        when ClassA.func2 is given a string parameter
          ✓ then ClassA.func2 is invoked with expected parameters, ClassA is constructed, ClassA.func1 is invoked with expected parameters (11 ms)
    
    Test Suites: 1 passed, 1 total
    Tests:       1 passed, 1 total
    Snapshots:   0 total
    Time:        0.508 s, estimated 1 s
    Ran all test suites related to changed files.
    

    package versions:

    "jest": "^29.7.0"