Reputation: 995
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
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
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
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
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
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
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
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
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