DEV Community

Cover image for Waiting for that important call
Ryan Kennedy
Ryan Kennedy

Posted on • Edited on • Originally published at rmkennedy.com

5 2

Waiting for that important call

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');
});
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
  )
/>)
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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!

Image of AssemblyAI tool

Challenge Submission: SpeechCraft - AI-Powered Speech Analysis for Better Communication

SpeechCraft is an advanced real-time speech analytics platform that transforms spoken words into actionable insights. Using cutting-edge AI technology from AssemblyAI, it provides instant transcription while analyzing multiple dimensions of speech performance.

Read full post

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Immerse yourself in a wealth of knowledge with this piece, supported by the inclusive DEV Community—every developer, no matter where they are in their journey, is invited to contribute to our collective wisdom.

A simple “thank you” goes a long way—express your gratitude below in the comments!

Gathering insights enriches our journey on DEV and fortifies our community ties. Did you find this article valuable? Taking a moment to thank the author can have a significant impact.

Okay