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)