Callback function not covered in test – how to mock?

I’m testing the following method:

  startScriptLoad(): void {
    const documentDefaultView = this.getDocumentDefaultView();
    if (documentDefaultView) {
      const twitterData: ICourseContentElementEmbedTweetWidgetData = this.getTwitterWidgetData() ?? {
        // eslint-disable-next-line @typescript-eslint/no-empty-function
        ready: () => {},
        _e: [],
      };
      if (this.scriptExists()) {
        ((this.document.defaultView as unknown) as ICourseContentElementEmbedTweetDocument)[
          this.TWITTER_OBJECT
        ] = twitterData;
        return;
      }
      this.appendScriptToDOM();
      twitterData._e = [];
      twitterData.ready = (callback: () => void) => {
        twitterData._e.push(callback);
      };
      ((this.document.defaultView as unknown) as ICourseContentElementEmbedTweetDocument)[
        this.TWITTER_OBJECT
      ] = twitterData;
    }
  }

with the following test:

  describe('startScriptLoad()', () => {
    it('should load script', () => {
      jest.spyOn(service, 'getDocumentDefaultView');
      jest.spyOn(service, 'appendScriptToDOM');
      service.startScriptLoad();
      expect(service.getDocumentDefaultView).toHaveBeenCalled();
      expect(service.appendScriptToDOM).toHaveBeenCalled();
    });
  });

For some reason, I can’t get coverage for the following lines of code in startScriptLoad():

  twitterData.ready = (callback: () => void) => {
    twitterData._e.push(callback);
  };

Is there a way that I can mock the call to the callback method somehow?

Answer

Use Proxy to create a proxy for the mocked object, which can intercept and redefine fundamental operations for that object. Which means we can use handler.set() set up a trap for setting the ready private method on mTwitterData object.

The trap is: assign the ready anonymous method to a variable like _ready. After getting it, call it manually for testing later.

service.ts:

export class SomeService {
  getTwitterWidgetData() {
    return {} as any;
  }
  startScriptLoad(): void {
    const twitterData = this.getTwitterWidgetData();
    twitterData._e = [];
    twitterData.ready = (callback: () => void) => {
      twitterData._e.push(callback);
    };
  }
}

service.test.ts:

import { SomeService } from './service';

describe('69041800', () => {
  test('should pass', () => {
    const service = new SomeService();
    let _ready;
    const mTwitterData = new Proxy({} as any, {
      set: (obj, prop, value) => {
        if (prop === 'ready') {
          _ready = value;
        }
        obj[prop] = value;
        return true;
      },
    });
    const getTwitterWidgetDataSpy = jest.spyOn(service, 'getTwitterWidgetData').mockReturnValue(mTwitterData);
    service.startScriptLoad();
    expect(getTwitterWidgetDataSpy).toBeCalledTimes(1);
    // test ready private method
    const mCallback = jest.fn();
    _ready(mCallback);
    expect(mTwitterData._e).toEqual([mCallback]);
    getTwitterWidgetDataSpy.mockRestore();
  });
});

test result:

 PASS  examples/69041800/service.test.ts (9.519 s)
  69041800
    ✓ should pass (4 ms)

------------|---------|----------|---------|---------|-------------------
File        | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
------------|---------|----------|---------|---------|-------------------
All files   |   83.33 |      100 |   66.67 |   83.33 |                   
 service.ts |   83.33 |      100 |   66.67 |   83.33 | 3                 
------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        10.148 s