How to write unit test for exponential backoff retry method implemented in javascript with jest

Viewed 2868

Trying to test an exponential backoff method which retries API request via fetch 5 times, will have the following delays: [1 ms, 10 ms, 100 ms, 1 s, 10 s], I am not able to successfully test it.

Methods

export const delay = retryCount => new Promise(resolve => setTimeout(resolve, 10 ** retryCount));

/**
 * Fetching with delay when api call fails,
 * first 5 retries will have the following delays: [1 ms, 10 ms, 100 ms, 1 s, 10 s]
 */
export const fetchRetry = async (options, retryCount = 0, lastError = null) => {
  if (retryCount > 5) throw new Error(lastError);
  try {
    return await fetch(options);
  } catch (error) {
    await delay(retryCount);
    return fetchRetry(options, retryCount + 1, error);
  }
};

Test

import fetchMock from 'jest-fetch-mock';

import { delay, fetchRetry } from './retry';

// This can be set up globally if needed
fetchMock.enableMocks();

beforeEach(() => {
  fetch.resetMocks();
});

describe('fetchWithExponentialBackoffRetry', () => {
  it('fetch is called once when response is 200', done => {
    fetch.mockResponseOnce(
      JSON.stringify({
        success: true,
        message: 'OK',
        code: 200,
        data: 'c86e795f-fe70-49be-a8fc-6876135ab109',
      }),
    );

    setTimeout(function() {
      fetchRetry({
        inventory_type_id: 2,
        advertiser_id: 2315,
        file: null,
      });
      expect(fetch).toHaveBeenCalledTimes(1);
      done();
    }, 0);
  });

  it('fetch is called 5 times when response is returns failure', done => {
    fetch.mockReject(() => Promise.reject(new Error('Rejected')));

    setTimeout(function() {
      fetchRetry({
        inventory_type_id: 2,
        advertiser_id: 2315,
        file: null,
      });
      expect(fetch).toHaveBeenCalledTimes(5);
      done();
    }, 100000);
  });
});

I am getting the following error

console.error node_modules/jsdom/lib/jsdom/virtual-console.js:29 Error: Error: connect ECONNREFUSED 127.0.0.1:8

I think it has to do we delay method I have to incorporate setTimeout somehow in my tests, now sure how to mock it here. I would appreciate the help.

1 Answers

You're testing the outcome of an async function, so you need to make your tests async too - you're not doing so - i.e. you're not awaiting fetchRetry and are therefore just calling done() synchronously.

I think the error is being caused by the use of setTimeout here. This looks like a race condition bug so hard to be sure without debugging, but from reading the code it looks like the issue is that you're mocking fetch with jest-fetch-mock, but since your test code runs synchronously and you have...

beforeEach(() => {
  fetch.resetMocks();
});

...it is probably unsetting the fetch mock before the call in the test that runs first is made and so it's actually calling your API - hence the error.

Making the tests async is fairly simple - the docs are here - and using async/await it's even cleaner since you don't actually need to use done - the test is just done when the promise resolves (or rejects).

Basically your test code will be mostly the same, except you'll be awaiting your calls to fetchRetry, like so:

it('fetch is called once when response is 200', async () => {
  fetch.mockResponseOnce(...)
     
  await fetchRetry({ ... }) 
  expect(fetch).toHaveBeenCalledTimes(1);
});

it('fetch is called 5 times when response is returns failure', async () => {
  fetch.mockReject(...);

  try {
    await fetchRetry({ ... });
  } catch (err) {
    // eventual error expected as response failure is mocked
    expect(fetch).toHaveBeenCalledTimes(5);  
  }  
});
Related