Search code examples
javascriptmocha.jstddsinon

How do I test a recursive, asynchronous JavaScript function using fake timers?


I have a basic recursive function, which works correctly when executed. I would like to use Sinon's fake timers to test the function at each stage of execution.

However, it seems that the fake timers are only applying to the first call of the recursive function.

I'm curious if anyone can help figure out how to get the fake timers working all the way through.

Example:

function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

async function ensureCount(count, { attempt, delay }) {
  attempt = attempt || 0
  console.log('Attempt', attempt)

  if (attempt === count) {
    return
  }

  await wait(delay)
  await ensureCount(count, { attempt: attempt + 1, delay })
}

Tested like so (with the help of this article):

it('retries after a given delay', function() {
  const clock = sinon.useFakeTimers()
  const promise = ensureCount(2, { delay: 200 })

  clock.tick(200)
  // Make some assertions.

  clock.tick(200)
  // Make some assertions.

  clock.tick(200)
  // Make some assertions.

  return promise
})

The expected console output (which is what happens without fake timers):

Attempt 0
Attempt 1
Attempt 2
✔ retries after a given delay (405ms)

The actual console output (which happens with fake timers):

Attempt 0
Attempt 1
Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.

The Question:

Is there a way to get the fake timers to apply to every step of the recursive call, rather than just the first one?


Solution

  • Turns out Sinon provides a .tickAsync() function for this that I hadn't seen. (Thanks to this comment).

    This code resolves the issue:

    it('retries after a given delay', async () => {
      const clock = sinon.useFakeTimers()
      ensureCount(2, { delay: 200 })
    
      await clock.tickAsync(200)
      // Make some assertions.
    
      await clock.tickAsync(200)
      // Make some assertions.
    
      await clock.tickAsync(200)
      // Make some assertions.
    
      clock.restore()
    })