0

I have the next code:

async function(paramA?: string): Promise<void> {
    if (paramA === undefined) {
      paramA = this.randomString();
    }
    this.funnel.loginFunnel(Status.Pending);
    await this.tracker.flush();
    this.service.call(this.name, paramA, code);
  }

And I want test that loginFunnel is called with status pending, and the service is call with the paramA, but this classes are initialized in constructor:

constructor(params: Params) {
    this.tracker = new Tracker(params);
    this.service = new Service(params, this.tracker);
  }

So how I can spy on with jest?, this is only javascript, not React or similar.

I try a lot of things, but I don't know how do...

The last try was this, import Tracker class from his path...

jest.mock('../tracker');
        service.call();
        expect(Tracker).toHaveBeenCalledTimes(1);

But I got this answer from test:

expect(received).toHaveBeenCalledTimes(expected)

    Matcher error: received value must be a mock or spy function

    Received has type:  function
    Received has value: [Function Tracker]
Lin Du
  • 88,126
  • 95
  • 281
  • 483
Juanma Perez
  • 97
  • 4
  • 14
  • 1
    This is hard because you've written code that isn't very testable. Your class is coupled to its collaborators, either invert the dependency or treat them as an internal implementation detail that shouldn't be replaced with test doubles. – jonrsharpe Aug 23 '22 at 20:46
  • Yes..., but the code is not mine, so I only have to do the testing – Juanma Perez Aug 24 '22 at 07:22

1 Answers1

-1

I understand your point. It's complex and 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.

  • This seems to be solely for external dependencies where `module-name` references the import. What about using classes that are within the repo being used and not in a separate module? – BrightIntelDusk Apr 25 '23 at 16:07