Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jest unit test - How do I call through async function in a setTimeout repeating function

UPDATE: Safe to say this is basically a duplicate of Jest: Timer and Promise don't work well. (setTimeout and async function)


I have a function that repeats itself after performing an asynchronous function. I want to verify that jest.advanceTimersOnTime(5000) continues to yield expect(doAsyncStuff).toHaveBeenCalledTimes(X);.

I observe that we can reach the call to repeat the function but it does not "go through" when asking jest to advance timers. It does work when you remove the async function preceding it. So it sounds like I have to get doAsyncStuff to "call through" or resolve some pending promise here?

Function

function repeatMe() {
    setTimeout(() => {
        doAsyncStuff.then((response) => {
            if (response) {
                console.log("I get here!");
                repeatMe();
            }
        })
    }, 5000);
}

Test

jest.useFakeTimers();

let doAsyncStuff = jest.spyOn(updater, 'doAsyncStuff');
doAsyncStuff.mockResolvedValue(true);

repeatMe();

jest.advanceTimersByTime(5000);
expect(doAsyncStuff).toHaveBeenCalledTimes(1);

jest.advanceTimersByTime(5000);
expect(doAsyncStuff).toHaveBeenCalledTimes(2); // Failed?
like image 328
ericjam Avatar asked Nov 05 '25 05:11

ericjam


1 Answers

So it sounds like I have to...resolve some pending promise here?

Yes, exactly.


The short answer is that a Promise callback gets queued in PromiseJobs by the then chained to the Promise returned by doAsyncStuff, and the way the test is written, that callback never has a chance to run until the test is already over.

To fix it, give the Promise callbacks a chance to run during your test:

updater.js

export const doAsyncStuff = async () => { };

code.js

import { doAsyncStuff } from './updater';

export function repeatMe() {
  setTimeout(() => {
    doAsyncStuff().then((response) => {
      if (response) {
        console.log("I get here!");
        repeatMe();
      }
    })
  }, 5000);
}

code.test.js

import * as updater from './updater';
import { repeatMe } from './code';

test('repeatMe', async () => {
  jest.useFakeTimers();

  let doAsyncStuff = jest.spyOn(updater, 'doAsyncStuff');
  doAsyncStuff.mockResolvedValue(true);

  repeatMe();

  jest.advanceTimersByTime(5000);
  expect(doAsyncStuff).toHaveBeenCalledTimes(1);  // Success!

  await Promise.resolve();  // let callbacks in PromiseJobs run

  jest.advanceTimersByTime(5000);
  expect(doAsyncStuff).toHaveBeenCalledTimes(2);  // Success!

  await Promise.resolve();  // let callbacks in PromiseJobs run

  jest.advanceTimersByTime(5000);
  expect(doAsyncStuff).toHaveBeenCalledTimes(3);  // Success!

  // ... and so on ...
});

The complete details of exactly what happens and why can be found in my answer here

like image 127
Brian Adams Avatar answered Nov 06 '25 19:11

Brian Adams



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!