freethinker
freethinker

Reputation: 2425

Jest deep method spy

How can I spy on publish and publishBatch inside an instance property:

Object.defineProperty(Provider, 'instance', {
    get: jest.fn(() => { 
        return {
            publish: jest.fn(),
            publishBatch: jest.fn()
        }
    }),
});

I'm aware of jest.spyOn(Provider, 'instance', 'get'); but I need to go deeper and couldn't find any information in the documentation.

Upvotes: 3

Views: 1343

Answers (2)

dropbeardan
dropbeardan

Reputation: 1160

A bit late on the response, but since I just looked this up myself, I wanted to share a strategy that I found to work.

# <some-dir>/your-code.js

import { SomeClass } from "./some-class";

const newClass = new SomeClass();

export function testTarget() {
  const deepMethodOne = newClass.classDeepMethodOne(1);
  const deepMethodTwo = deepMethodOne.classDeepMethodTwo(2);
  return true;
};
# <some-dir>/your-test.js

import { testTarget } from "./your-code";

jest.mock(("./some-class") => {
  class MockSomeClass {
    constructor() {}

    classDeepMethodOne = (...args) => mockClassDeepMethodOne(...args);
  }

  return {
    SomeClass: MockSomeClass
  }
});


describe("Testing Your Code", () => {
  const mockClassDeepMethodOne = jest.fn(() => ({
    classDeepMethodTwo: mockClassDeepMethodTwo
  }));

  const classDeepMethodTwo = jest.fn();

  it("spies on mocked deep method one", () => {
    testTarget();
    expect(mockClassDeepMethodOne).toHaveBeenCalledWith(1);
  });

  it("spies on mocked deep method two", () => {
    testTarget();
    expect(mockClassDeepMethodTwo).toHaveBeenCalledWith(2);
  })
});

The notion behind why this works is that this gets around Jest's import-level hoisting restrictions by instantiating an immediate binding to data structures that would cause a delayed effect (e.g. functions).

A little more in depth: Jest hoists your import-level mocks prior to the imports, so it does not allow you to have any immediate bindings to any unhoisted code, e.g.:

# Player 1: Go to jail, do not pass go.

class MockSomeClass {
  constructor() {}

  classDeepMethodOne = (...args) => mockClassDeepMethodOne(...args);
}

jest.mock(("./some-class") => ({ SomeClass: MockSomeClass }));
# Player 2: Go to jail, do not pass go.

jest.mock(("./some-class") => {
  class MockSomeClass {
    constructor() {}

    classDeepMethodOne = mockClassDeepMethodOne;
  }

  return {
    SomeClass: MockSomeClass
  }
});

Don't ask my why, I have no idea, probably a chicken and egg problem.

Upvotes: 0

freethinker
freethinker

Reputation: 2425

The solution is much easier than I thought:

const obj = {
    publish: jest.fn(),
    publishBatch: jest.fn()
}

Object.defineProperty(Provider, 'instance', {
    get: jest.fn(() => { 
        return obj;
    }),
});

const publishSpy = jest.spyOn(obj, 'publish');

...

expect(publishSpy).toHaveBeenCalled();

Upvotes: 2

Related Questions