Hold ON a minute

Using recursive async functions to delay Jest tests, because sometimes you just need Jest to WAIT, SHEESH

Let’s say you’re testing a function that:

  • Does not accept callbacks (so you can’t use done as described previously)
  • Is not async and doesn’t return a Promise
  • Does not fire events or otherwise notify the outside world that something happened
  • Does behave asynchronously, by e.g. calling external APIs or waiting on internal events

Further, let’s say you need to test that something happens some time after the function is called. How can you tell your Jest test to wait for a certain condition to be true before you actually make your test’s assertions?

Recursion to the rescue! To solve this problem in my own test setup, I wrote an recursive async function that repeatedly checks the state of the system and only resolves after the desired condition is met.

I used this trick to convert all my tests that used callbacks to much more expressive, cleaner tests that use async/await. Tests went from this:

test('expectations will only pass after a certain condition is met', done => {
  systemUnderTest(() => { // Some funky logic in `systemUnderTest` handles this callback
    if (!systemIsInCorrectState) return
    expect() //... a bunch of `expect` calls
    done() // Tell Jest we're done here
})Code language: JavaScript (javascript)

…to this:

test('expectations will only pass after a certain condition is met', async () => {
  systemUnderTest() // No callbacks!

  await awaitSystemCondition(condition) // Tell the test to hold its dang horses!

  expect() // Only make assertions after horses have been held!
})Code language: JavaScript (javascript)

This looks a lot better to me. It’s much clearer what’s going on. And if that await never fulfills, Jest will fail the test!

More concretely, I have a normal synchronous function that generates a Robot state machine. The state machine transitions through its states automatically (and asynchronously, by reacting to internal events).

Since I want to test the function that creates the machine—not the machine itself—I need to wait for the machine to be in a stable state to assert that expected actions were taken (e.g. fetch was called).

If I were testing the machine itself, I might use a callback whenever the machine state changes, since Robot’s API allows us to pass in a callback. But there are no callbacks here — just a regular ole function! I can inspect the current state of the machine at any point after it’s created, using Robot’s service object.

So how do we tell Jest to wait?

It starts with the classic setTimeout trick, written as an async function:

const sleep = async ms => new Promise(resolve => setTimeout(resolve, ms))Code language: JavaScript (javascript)

Now we can drop this little fella into any async context to move the function execution later in the call stack.

With that up our sleeve, we can create a recursive async function that checks the state of our state machine until it reaches the desired state. If we’re in the desired state, return; if not, move down the call stack and try again. A non-blocking recursive function!

const awaitMachineState = async (state, service) => {
  if (service.machine.current === state) { // Check the current machine state against our desired state
    return state

  await sleep(0) // move down the stack if the above comparison is false

  return awaitMachineState(state, service) // try again
}Code language: JavaScript (javascript)

Using awaitMachineState, we can write an async test that waits for the machine to get to its final, stable state before running any assertions.

test('it fetches SKUs after instantiation', async () => {
  const fetchSpy = jest.spyOn(global, 'fetch')
  const service = makeService() // this does some work and returns a state machine
  await awaitMachineState('displaySkus', service) // BADA BING

})Code language: JavaScript (javascript)

I like this approach. The test expresses something very clear, especially compared to using callbacks. It fails clearly as well: if the state is never reached, the test fails.

Hypothetically, in my case, this could lead to a race condition, particularly if I were waiting for some intermediary state, but so far, it hasn’t been a problem! (I’m always waiting for a final state, after all.)

Is this the best way to do this? I don’t know! I’m still learning Jest! And I feel like I’m writing “real” JS for the first time in ages, so I’m a bit rusty. Maybe there’s a more efficient/better/idiomatic way to do this. Tell me if there is, because I’d love to know.

PS: Couldn’t have figured this out without the more thorough write-up on using asynchronous recursion on the ScottLogic blog.