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!
Top comments (0)