Search code examples
javascripttypescriptunit-testingjestjs

jest.advanceTimersByTime doesn't work when I try to test my retry util function


I have a retry util function I wanted to test for. It looks like this

export const sleep = (t: number) => new Promise((r) => setTimeout(r, t));

type RetryFn = (
  fn: Function,
  config: {
    retryIntervel: number;
    retryTimeout: number;
    predicate: Function;
    onRetrySuccess?: Function;
    onRetryFail?: Function;
  }
) => Promise<any>;

export const retry: RetryFn = async (
  fn,
  { predicate, onRetrySuccess, onRetryFail, retryIntervel, retryTimeout }
) => {
  const startTime = Date.now();
  let retryCount = 0;
  while (Date.now() - startTime < retryTimeout) {
    try {
      const ret = await fn();
      if (predicate(ret)) {
        if (retryCount > 0) onRetrySuccess && onRetrySuccess();
        return ret;
      } else {
        throw new Error();
      }
    } catch {
      retryCount++;
    }
    await sleep(retryIntervel);
  }
  if (onRetryFail) onRetryFail();
};

what it does is retry the function for a period of time at a given interval.

I thought I could use jest.advanceTimersByTime to advance the timer to test how many times the retry happens.

import { retry } from "./index";

const value = Symbol("test");

function mockFnFactory(numFailure: number, fn: Function) {
  let numCalls = 0;
  return function () {
    fn();
    numCalls++;
    if (numCalls <= numFailure) {
      console.log("numCalls <= numFailure");

      return Promise.resolve({ payload: null });
    } else {
      console.log("numCalls => numFailure");

      return Promise.resolve({
        payload: value
      });
    }
  };
}

describe("retry function", () => {
  beforeEach(() => {
    jest.useFakeTimers();
  });
  it("retrys function on 1st attempt, and succeed thereafter", async () => {
    const fn = jest.fn();
    const onRetrySuccessFn = jest.fn();
    const mockFn = mockFnFactory(3, fn);
    retry(mockFn, {
      predicate: (res: any) => res.payload === value,
      onRetrySuccess: onRetrySuccessFn,
      retryIntervel: 1000,
      retryTimeout: 5 * 60 * 1000
    });
    jest.advanceTimersByTime(1000);
    expect(fn).toHaveBeenCalledTimes(1);
    expect(onRetrySuccessFn).not.toHaveBeenCalled();
    jest.advanceTimersByTime(1000);
    expect(fn).toHaveBeenCalledTimes(2); // 🚨 fail
    expect(onRetrySuccessFn).not.toHaveBeenCalled();
    jest.advanceTimersByTime(2000);
    expect(fn).toHaveBeenCalledTimes(3);// 🚨 fail
    expect(onRetrySuccessFn).toHaveBeenCalledTimes(1);
  });
});

but it seems like no matter how much I advanced the timer, the function only gets invoked once.

You can find the code on codesandbox at https://codesandbox.io/s/lucid-knuth-e810e?file=/src/index.test.ts

However, there is a known issue with codesandbox where it keeps throwing this error TypeError: jest.advanceTimersByTime is not a function . This error doesn't appear locally.


Solution

  • It's because of this.

    Here's what I use in a test helpers file:

    const tick = () => new Promise(res => setImmediate(res));
    
    export const advanceTimersByTime = async time => jest.advanceTimersByTime(time) && (await tick());
    
    export const runOnlyPendingTimers = async () => jest.runOnlyPendingTimers() && (await tick());
     
    export const runAllTimers = async () => jest.runAllTimers() && (await tick());
    

    In my test file, I import my helpers and instead of calling jest.advanceTimersByTime, I await my advanceTimersByTime function.

    In your specific example, you just need to await a function after calling advanceTimersByTime - like this:

    // top of your test file
    const tick = () => new Promise(res => setImmediate(res));
    
    ... the rest of your existing test file
    
        jest.advanceTimersByTime(1000);
        expect(fn).toHaveBeenCalledTimes(1);
        expect(onRetrySuccessFn).not.toHaveBeenCalled();
        jest.advanceTimersByTime(1000);
        await tick(); // this line
        expect(fn).toHaveBeenCalledTimes(2);
        expect(onRetrySuccessFn).not.toHaveBeenCalled();
        jest.advanceTimersByTime(2000);
        await tick(); // this line
        expect(fn).toHaveBeenCalledTimes(3)
        expect(onRetrySuccessFn).toHaveBeenCalledTimes(1);