Jonathan Tse
Jonathan Tse

Reputation: 995

How to spy on a class constructor jest?

export class Foo {
    public static bar() {
        doSomething();
    }

    constructor(paramA, paramB) {

    } 
}

For a method in a class, we can use jest.spyOn(Foo, 'bar') to spy on the method. How about constructor? How do we spy how the object is instantiated?

Upvotes: 55

Views: 78672

Answers (8)

Gustavo Gonçalves
Gustavo Gonçalves

Reputation: 493

I understand your point, and I took some time wondering for a solution. I ended up creating this code snippet, which can spyOn an entire class, including its constructors. And the usage is also somehow simple, you can add this snippet to a file and import it whenever you need.

Here is the code (typescript/ES6):

/**
 * spyOn references to classes. Use it with spyOnClass
 */
export const classSpy: any = {};

/**
 * Utility to Spy On all class methods. Not including the constructor
 * @returns a spyOn references to all the class methods
 * includes the methods mockClear and mockReset as convenience
 * to trigger the respective method for all the spies
 */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function spyOnClassMethods(proto: any): any {
    const properties = Object.getOwnPropertyNames(proto);
    const spyHolder: any = {};
    for (const i in properties) { spyHolder[properties[i]] = jest.spyOn(proto, properties[i]); }
    spyHolder.mockClear = (): void => { for (const i in properties) { spyHolder[properties[i]].mockClear(); } };
    spyHolder.mockReset = (): void => { for (const i in properties) { spyHolder[properties[i]].mockReset(); } };
    return spyHolder;
}
// To attend jest.mock problems, the should start with 'mock'
const mocksSpyOnClassMethods = spyOnClassMethods;

/**
 * Utility to Spy On all class methods and its constructor.
 */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function spyOnClass(mockModuleName: string, mockClassName: string): any {
    classSpy[mockClassName] = {};
    jest.mock(mockModuleName, () => {
        const module = jest.requireActual(mockModuleName) as any;
        const mock = {};
        classSpy[mockClassName] = mocksSpyOnClassMethods(module[mockClassName].prototype);
        mock[mockClassName] = jest.fn().mockImplementation(
            (...args: any[]) => {
                const instance = new module[mockClassName](...args);
                classSpy[mockClassName].constructor = mock[mockClassName];
                return instance;
            }
        );
        return { ...module, ...mock };
    });
}

Usage example:

import { classSpy, spyOnClass } from './mock-utils';

// If you import ClassName, this must come before the import.
spyOnClass('module-name', 'ClassName');

import { ClassName } from 'module-name';

test('this', () => {
    doSomethingThatUsesClassName();
    expect(classSpy.ClassName.constructor).toHaveBeenCalled();
    expect(classSpy.ClassName.someMethod).toHaveBeenCalled();
});

Hope it can help you and others.

Upvotes: 1

Cristiano Santos
Cristiano Santos

Reputation: 2137

I created an helper function to do this for multiple classes of mine:

export function MockConstructorSpy(module: string, className: string, isDefault: boolean) {
  const spyMethod = jest.fn();
  jest.mock(module, () => {
    let mockClass = null;
    if (isDefault) {
      mockClass = jest.requireActual(module).default;
    } else {
      const {[className]: mockedModuleClass} = jest.requireActual(module);
      mockClass = mockedModuleClass;
    }
    class Mock extends mockClass {
      constructor(...args: any) {
        super(...args);
        spyMethod(...args);
      }
    }
    if (isDefault) return Mock;
    else return {[className]: Mock};
  });
  return spyMethod;
}

Afterwards I do the following for each class that I want to spy for its constructor

const {MockConstructorSpy} = require('../mock-constructor-spy');
const spyCredentialCreation = MockConstructorSpy('./credential-request', 'CredentialRequest', false);
// The import must be done after the call to MockConstructorSpy
import {CredentialRequest} from './credential-request';

it('should catch the constructor call', () => {
  const a = new CredentialRequest('a', 'b');
  expect(spyCredentialCreation).toHaveBeenCalled();
  expect(a.email).toBe('a');
});

Upvotes: 3

N4N0
N4N0

Reputation: 321

@gillyb is right just forgot to "mock" the Foo module

// Foo.js
export class Foo {
    public static bar() {
        doSomething();
    }

    constructor(paramA, paramB) {

    } 
}

// Foo.spec.js
import Foo from './Foo.js';
jest.mock('./Foo.js');

it('test something...', () => {
    // Assuming we did something that called our constructor
    expect(Foo).toHaveBeenCalledTimes(1);
});

Upvotes: 28

Nati Kamusher
Nati Kamusher

Reputation: 761

If you have a need, for example, to validate the arguments passed to the constructor, you don't necessarily have to mock the ctor' directly.

I created a static method within the ctor's class e.g.

 static checkData(data) {/*some validation on data*/}

and mocked it normally with jest.spy.mockImplementation, implementing some validation on the data parameter.

In the ctor' then, I call this checkData function with whatever input I want, specifically the arguments of the ctor'.

And walla, ctor's args are validated without mocking the ctor itself.

Upvotes: 0

Fomentia
Fomentia

Reputation: 994

If you actually want to spy on the constructor, you can do something like this:

// MyClass.js

export class MyClass {
  constructor() {
    console.log("constructing");
  }
}

// MyClass.test.js

import * as MyClassModule from './MyClass';

const MyClass = MyClassModule.MyClass;

test('the constructor is called', () => {
  const constructorSpy = jest.spyOn(MyClassModule, 'MyClass');
  new MyClass();
  expect(constructorSpy).toHaveBeenCalledTimes(1);
});

Upvotes: 29

Ravi
Ravi

Reputation: 69

// Actual Implementation 
import { ModuleToMock} from 'module-to-mock'

const test = new ModuleToMock('value1', 'value2')

test.setUser({ name: 'test', address: 'myTest' })

// test case using jest 
 import { ModuleToMock} from 'module-to-mock'
 jest.mock('module-to-mock')

// constructor response 
   const mockedModuleResponse = {
   setUser: jest.fn(), 
   }

ModuleToMock.mockImplementation(() => mockedModuleResponse)

Reference from How to Use Jest to Mock Constructors

Upvotes: 3

gillyb
gillyb

Reputation: 8910

Actually there is a way :)
It's even in the official docs: https://jestjs.io/docs/en/es6-class-mocks#complete-example

Here's how you would do it with your code:

// Foo.js
export class Foo {
    public static bar() {
        doSomething();
    }

    constructor(paramA, paramB) {

    } 
}

// Foo.spec.js
import Foo from './Foo.js';

it('test something...', () => {
    // Assuming we did something that called our constructor
    expect(Foo).toHaveBeenCalledTimes(1);
});

Upvotes: -1

Posva
Posva

Reputation: 1102

I think there's no official way. This is how I personally do it:

const spy = jest.fn()
function Mock (...args) {
  spy(...args)
  Constructor.apply(this, args)
}
Mock.prototype = Constructor.prototype

Then I can check the spy:

expect(spy).toHaveBeenCalledWith('firstArg', 'secondArg')

Upvotes: 3

Related Questions