Waiting for that important call

Originally published Mar 14, 2018·Tagged #javascript, #testing

Sometimes while testing, it's necessary to wait until a function has been called. Maybe you're testing code with Node-style callbacks; maybe you're working with a React render prop. Regardless of how you got there, your test needs to pause until some function has been called. It's possible to wait for a promise to be fulfilled, but how do you wait until an arbitrary function has been called?

The problem

Suppose your test looks like this:

const createEmitterOfSomeSort = require('./myEmitter');

it('should do the thing', async () => {
  const emitter = createEmitterOfSomeSort();
  const callback = jest.fn();
  emitter.on('my-event', callback);
  
  // TODO: wait for the callback to be called before proceeding

  // Check values which will only change after the given event
  expect(emitter.color).toBe('blue');
});

This test needs to wait for my-event to be fired asynchronously before the color gets set. Otherwise, the test prematurely races through to its completion.

It's possible to wrap this all in a Promise which will resolve when your event is fired. I've done this loads of times in tests; it's tedious! It's also a pain to refactor. Suppose you want to wait for the event to fire 5 times instead of just once. This requires additional work and added complexity to your test.

My attempted solution

I decided to write and publish my solution as the anticipated-call package. This utility is capable of wrapping any function, and gives you an easy way to obtain a promise which resolves once the function has been called.

Here's an example of how you might use it in a test:

const anticipated = require('anticipated-call');
const createEmitterOfSomeSort = require('./myEmitter');

it('should do the thing', async () => {
  const emitter = createEmitterOfSomeSort();
  const callback = anticipated(jest.fn());
  emitter.on('my-event', callback);
  
  await callback.nextCall;

  // Check values which will only change after the given event
  expect(emitter.color).toBe('blue');
});

The await statement is the magic sauce: it'll pause the test's execution until the callback is called.

Now, if you decide the event needs to be fired 5 times instead of just once, it's simple to update your tests:

  await callback.nthNextCall(5);

Testing React render props

This package has helped me the most when I'm writing render-prop components. Suppose you have a component responsible for fetching data that's used like this:

(<MyTweetFetcher
  render={({isLoading, username, tweets}) => (
    <h2>{isLoading ? 'Loading...' : username}</h2>
    <ul>
      {tweets.map((tweet) => (
        <li key={tweet.id}>{tweet.content}</li>
      )}
    </ul>
  )
/>)

These components commonly call the render prop multiple times in response to asynchronous operations. This behavior creates a problem for writing tests: you need to make sure that the callback received the correct arguments, but you can't perform that check until the component has been rendered. anticipated-call comes to the rescue:

const Enzyme = require('enzyme');
const anticipated = require('anticipated-call');

const MyTweetFetcher = require('./MyTweetFetcher');

it('should call the render prop with the correct arguments', async () => {
  // The render prop needs to return a valid React node, so use `null` here.
  const renderProp = anticipated(jest.fn(() => null));

  // The `nextCallDuring` method allows you to tell `anticipated-call` that
  // the function should be called as a result of running the passed callback.
  await renderProp.nextCallDuring(() => {
    Enzyme.mount(<MyTweetFetcher render={renderProp} />);
  });
  
  // The render prop will initially be called while data is loading.
  expect(renderProp.mock.calls[0].isLoading).toBe(true);

  // Wait for the render prop to be called again, after the data has loaded.
  await renderProp.nextCall;

  expect(renderProp.mock.calls[1].isLoading).toBe(false);
  expect(renderProp.mock.calls[1].tweets).toBeInstanceOf(Array);
});

Friendlier testing

This package is pretty small; it does nothing that can't already be done with a bit of Promise-wrangling. However, its appeal lies in the fact that you no longer have to engage in any Promise-wrangling. When I need to wait for a callback, I throw anticipated-call at it and save my energy for more difficult problems.

Check out anticipated-call on npm and submit PRs or issues on Github if you have ideas for improving it!