How to use JEST to test the time course of recursi

2019-07-12 13:43发布

问题:

I write a test using JEST. I do not know how to test promise recursion in JEST.

In this test, the retry function that performs recursion is the target of the test until the promise is resolved.

export function retry<T>(fn: () => Promise<T>, limit: number = 5, interval: number = 1000): Promise<T> {
  return new Promise((resolve, reject) => {
    fn()
      .then(resolve)
      .catch((error) => {
        setTimeout(() => {
          // Reject if the upper limit number of retries is exceeded
          if (limit === 1) {
            reject(error);

            return;
          }
          // Performs recursive processing of callbacks for which the upper limit number of retries has not been completed
          try {
            resolve(retry(fn, limit - 1, interval));
          } catch (err) {
            reject(err);
          }
        }, interval);
      });
  });
}

Perform the following test on the above retry function.

  1. retry() is resolved in the third run. The first, second and third times are called every 1000 seconds respectively.

I thought it would be as follows when writing these in JEST.


jest.useFakeTimers();

describe('retry', () => {
  // Timer initialization for each test
  beforeEach(() => {
    jest.clearAllTimers();
  });
  // Initialize timer after all tests
  afterEach(() => {
    jest.clearAllTimers();
  });

  test('resolve on the third call', async () => {
    const fn = jest
      .fn()
      .mockRejectedValueOnce(new Error('Async error'))
      .mockRejectedValueOnce(new Error('Async error'))
      .mockResolvedValueOnce('resolve');

    // Test not to be called
    expect(fn).not.toBeCalled();
    // Mock function call firs execution
    await retry(fn);
    // Advance Timer for 1000 ms and execute for the second time
    jest.advanceTimersByTime(1000);
    expect(fn).toHaveBeenCalledTimes(2);
    // Advance Timer for 1000 ms and execute for the third time
    jest.advanceTimersByTime(1000);
    expect(fn).toHaveBeenCalledTimes(3);

    await expect(fn).resolves.toBe('resolve');
  });

});

As a result, it failed in the following error.

● retry › resolve on the third call
Timeout - Async callback was not invoked within the 30000ms timeout specified by jest.setTimeout.Error: 

    > 16 |   test('resolve on the third call', async () => {
         |   ^
      17 |     jest.useFakeTimers();
      18 |     const fn = jest
      19 |       .fn()

I think that it will be manageable in the setting of JEST regarding this error. However, fundamentally, I do not know how to test promise recursive processing in JEST.

回答1:

Your function is so hard to testing with timer.

When you call await retry(fn); this mean you will wait until retry return a value, but the setTimeout has been blocked until you call jest.advanceTimersByTime(1000); => this is main reason, because jest.advanceTimersByTime(1000); never been called.

You can see my example, it is working fine with jest's fake timers.

  test("timing", async () => {
    async function simpleTimer(callback) {
      await callback();
      setTimeout(() => {
        simpleTimer(callback);
      }, 1000);
    }

    const callback = jest.fn();
    await simpleTimer(callback); // it does not block any things
    for (let i = 0; i < 8; i++) {
      jest.advanceTimersByTime(1000); // then, this line will be execute
      await Promise.resolve(); // allow any pending jobs in the PromiseJobs queue to run
    }
    expect(callback).toHaveBeenCalledTimes(9);  // SUCCESS
  });

I think, you could skip test timer detail, just test about you logic: fn has been called 3 times, finally it return "resolve"

test("resolve on the third call", async () => {
    const fn = jest
      .fn()
      .mockRejectedValueOnce(new Error("Async error"))
      .mockRejectedValueOnce(new Error("Async error"))
      .mockResolvedValueOnce("resolve");

    // expect.assertions(3);

    // Test not to be called
    expect(fn).not.toBeCalled();
    // Mock function call firs execution

    const result = await retry(fn);

    expect(result).toEqual("resolve");

    expect(fn).toHaveBeenCalledTimes(3);
  });

Note: remove all your fake timers - jest.useFakeTimers



回答2:

The documentation of jest suggests that testing promises is fairly straightforward ( https://jestjs.io/docs/en/asynchronous )

They give this example (assume fetchData is returning a promise just like your retry function)

test('the data is peanut butter', () => {
  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});