Search code examples
javascripttypescriptvue.jsunit-testingvitest

How to spy on Vue composable inner function that is called inside another function inside composable?


The project is using Vue, Vitest and Vue Test Utils. I am new to UI unit testing and I need to test that when one function from composable is called, then the other function should have been called.

My functions look something like this (details omitted):


export function useHelper() {

  async function wait(milliseconds: number): Promise<void> {
    return await new Promise<void>(resolve => setTimeout(() => resolve(), milliseconds));
  }

  async function executeInBatches(delay = 0) {
    if (delay) {
      await wait(delay);
    }
  }

return {
    executeInBatches,
    wait,
  };
}


My unit test is next:


beforeEach(() => {
  vi.useFakeTimers({shouldAdvanceTime: true});
});

afterEach(() => {
  vi.useRealTimers();
  vi.clearAllMocks();
});


 it('should call function wait when delay is provided', async () => {
      const composable = useHelper();
      const waitSpy = vi.spyOn(composable, 'wait');
      const delay = 100;

      await executeInBatches(delay);
      vi.advanceTimersByTime(delay * 2);

      expect(waitSpy).toHaveBeenCalled();
    });

I do not understand why waitSpy does not work. If i do exactly same thing, but spy on window.setTimeout, which is called inside wait function, then the timeout spy correctly works.

Working example with spying on setTimeout:


 it('should call setTimeout when delay is provided', async () => {
      const composable = useHelper();
      const timeoutSpy = vi.spyOn(window 'setTimeout')
      const delay = 100;

      await executeInBatches(delay);
      vi.advanceTimersByTime(delay * 2);

      expect(timeoutSpy).toHaveBeenCalled();
    });

Could you please help, why does spying on composable does not work?

Thanks in advance


Solution

  • This is impossible without changing how JavaScript works. executeInBatches always refers wait local function. And if wait weren't returned, it couldn't be accessed from the outside at all. In order to change this, wait should have been referred as some object method, e.g. this.wait, which is unsuitable here because the results of composables are conventionally destructable. This is what happens in the second test, where globalThis.setTimeout is accessed.

    In order to be testable, wait should be moved to a separate module. It looks like general utility function any way, so it could be reused through the project. This way it can be spied or mocked with vi.mock.

    Otherwise useHelper needs to be treated a single unit in tests, this is not a problem because Jest/Vitest fake timers allow to effectively control timer-related code.