Search code examples
reactjsmockingjestjsaxioses6-promise

Flushing a Promise queue?


While writing tests using Jest, an asynchronous mocked Axios request was causing a setState() call after a component was mounted. This resulted in console output during the test run.

it('renders without crashing', () => {
  const mockAxios = new MockAdapter(axios);

  mockAxios.onGet(logsURL).reply(200, [...axios_logs_simple]);

  const div = document.createElement('div');
  ReactDOM.render(<Logs />, div);
  ReactDOM.unmountComponentAtNode(div);
});

And in the <Logs> component:

componentDidMount() {
  axios.get(apiURL).then(response => {
    this.setState({data: response.data});
  });
}

I found a blog post showing a solution, but I don't understand how it does anything useful:

it('renders without crashing', async () => { //<-- async added here
  const mockAxios = new MockAdapter(axios);

  mockAxios.onGet(logsURL).reply(200, [...axios_logs_simple]);

  const div = document.createElement('div');
  ReactDOM.render(<Logs />, div);
  await flushPromises(); //<-- and this line here.
  ReactDOM.unmountComponentAtNode(div);
});

const flushPromises = () => new Promise(resolve => setImmediate(resolve));

As I understand it, it creates a new promise that resolves immediately. How can that guarantee other asynchronous code will resolve?

Now, it solved the issue. There are no more console messages during the test run. The test still passes (it's not a very good one). But this just seems like I just landed on the other side of a race condition, rather than preventing the race condition in the first place.


Solution

  • I will start with the reason why do you need this solution.

    The reason is that you don't have the reference to the async task and you need your test assertion to run after the async task. If you had it, you can just await on it, this will assure that your test assertion will run after the async task. (in your case it is ReactDOM.unmountComponentAtNode)

    In your example the async task is inside componentDidMount which react calls, so you don't have the reference to the async task.

    Usually a simple implementation of flushPromises will be:

    const flushPromises = () => new Promise(resolve => setImmediate(resolve));
    

    This implementation is based on the fact that async operations in javascript are based on task queue (javascript has several types of queues for async tasks). Specifically, promises are queued in a micro-task queue. Each time an async operation (such as http request) finishes the async task dequeues and executes.

    setImmediate is a method that gets a callback and stores it on the Immediates Queue and will be called in the next iteration of the event loop. This Immediates Queue is checked after the Micro-task queue (which holds the promises).

    Let's analyze the code, we are creating a new promise which is enqueued at the end of the Micro-task queue, it's resolve will be called on the next iteration of the event loop, that means it will be resolved after all the promises that are already enqueued.

    Hope this explanation helps.

    Pay attention that if inside of the async task you will enqueue a new promise, it will enter the queue at the end, that means that the promise that flushPromises return will not run after it.

    Few posts / videos for more info:

    1. https://blog.insiderattack.net/promises-next-ticks-and-immediates-nodejs-event-loop-part-3-9226cbe7a6aa
    2. https://www.youtube.com/watch?v=8aGhZQkoFbQ
    3. https://www.youtube.com/watch?v=cCOL7MC4Pl0