Reputation: 659
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.
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.
Upvotes: 3
Views: 3481
Reputation: 404
I agree with @hoangdv this recursive setTimeout pattern is very tricky to test. This is the only solution that worked for me after trying @hoangdv's (which gave me much inspiration).
Note I'm using a similar retry pattern around network Fetches. I'm using React Testing Library to look for an error message that only appears when the retry function ran 3 times.
it('Shows file upload error on timeout', async () => {
jest.useFakeTimers();
setupTest();
// sets up various conditions that trigger the underlying fetch
while (
screen.queryByText(
'There appears to be a network issue. Please try again in a few minutes.'
) === null
) {
jest.runOnlyPendingTimers(); // then, this line will be execute
await Promise.resolve(); // allow any pending jobs in the PromiseJobs queue to run
}
await waitFor(() => {
expect(
screen.getByText(
'There appears to be a network issue. Please try again in a few minutes.'
)
).toBeInTheDocument();
});
jest.useRealTimers();
});
Upvotes: 0
Reputation: 16147
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
Upvotes: 4
Reputation: 976
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');
});
});
Upvotes: 1