22
  • React v15.1.0
  • Jest v12.1.1
  • Enzyme v2.3.0

I'm trying to figure out how to test a component that calls a promise in a function invoked by a click. I was expecting Jest's runAllTicks() function to help me out here, but it doesn't seem to be executing the promise.

Component:

import React from 'react';
import Promise from 'bluebird';

function doSomethingWithAPromise() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, 50);
  });
}

export default class AsyncTest extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      promiseText: '',
      timeoutText: ''
    };

    this.setTextWithPromise = this.setTextWithPromise.bind(this);
    this.setTextWithTimeout = this.setTextWithTimeout.bind(this);
  }

  setTextWithPromise() {
    return doSomethingWithAPromise()
      .then(() => {
        this.setState({ promiseText: 'there is text!' });
      });
  }

  setTextWithTimeout() {
    setTimeout(() => {
      this.setState({ timeoutText: 'there is text!' });
    }, 50);
  }

  render() {
    return (
      <div>
        <div id="promiseText">{this.state.promiseText}</div>
        <button id="promiseBtn" onClick={this.setTextWithPromise}>Promise</button>
        <div id="timeoutText">{this.state.timeoutText}</div>
        <button id="timeoutBtn" onClick={this.setTextWithTimeout}>Timeout</button>
      </div>
    );
  }
}

And the tests:

import AsyncTest from '../async';
import { shallow } from 'enzyme';
import React from 'react';

jest.unmock('../async');

describe('async-test.js', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallow(<AsyncTest />);
  });

  // FAIL
  it('displays the promise text after click of the button', () => {
    wrapper.find('#promiseBtn').simulate('click');

    jest.runAllTicks();
    jest.runAllTimers();

    wrapper.update();

    expect(wrapper.find('#promiseText').text()).toEqual('there is text!');
  });

  // PASS
  it('displays the timeout text after click of the button', () => {
    wrapper.find('#timeoutBtn').simulate('click');

    jest.runAllTimers();

    wrapper.update();

    expect(wrapper.find('#timeoutText').text()).toEqual('there is text!');
  });
});
Caspar
  • 410
  • 1
  • 3
  • 11
  • you can instead pass doSomethingWithAPromise function as a prop to AsyncTest component so that you can mock it inside your test: http://stackoverflow.com/questions/38308214/react-enzyme-test-componentdidmount-async-call/40875174#40875174 – bodrin Nov 29 '16 at 20:44

2 Answers2

67

Updated answer: using async / await leads to cleaner code. Old code below.

I've successfully solved this problem by combining the following elements:

  • Mock out the promise and make it resolve immediately
  • Make the test asynchronous by marking the test function async
  • After simulating the click, wait until the next macrotask to give the promise time to resolve

In your example, that might look like this:

// Mock the promise we're testing
global.doSomethingWithAPromise = () => Promise.resolve();

// Note that our test is an async function
it('displays the promise text after click of the button', async () => {
    wrapper.find('#promiseBtn').simulate('click');
    await tick();
    expect(wrapper.find('#promiseText').text()).toEqual('there is text!');
});

// Helper function returns a promise that resolves after all other promise mocks,
// even if they are chained like Promise.resolve().then(...)
// Technically: this is designed to resolve on the next macrotask
function tick() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  })
}

Enzyme's update() is neither sufficient nor needed when using this method, because Promises never resolve in the same tick they are created -- by design. For a very detailed explanation of what is going on here, see this question.

Original answer: same logic but slightly less pretty. Use Node's setImmediate to defer the test until the next tick, which is when the promise will resolve. Then call Jest's done to finish the test asynchronously.

global.doSomethingWithAPromise = () => Promise.resolve({});

it('displays the promise text after click of the button', (done) => {
    wrapper.find('#promiseBtn').simulate('click');

  setImmediate( () => {
    expect(wrapper.find('#promiseText').text()).toEqual('there is text!');
    done();
  })
});

This isn't as nice because you'll get big nested callbacks if you have to wait for more than one promise.

Jonathan Stray
  • 2,708
  • 2
  • 26
  • 32
  • 1
    BINGO! `setImmediate` was what I needed to use in my callback function which was being triggered by way of one of my component's `componentDidUpdate` lifecycle methods. – Ryan H. Aug 31 '17 at 16:19
  • I had a similar problem but I solved it with `setTimeout(() => { ... }, 0)` instead. For some reason `setImmediate` made the tests fail in a way that even the `yarn test` command exited abruptly. – Ernesto Oct 03 '17 at 12:43
  • 1
    thank you for this! I called `done()` outside of my setImmediate, but this combination works & prevents test suite from hanging. – bryce Apr 18 '18 at 20:24
  • The solution looks promising but in my case setImmediate does not have any effect. The promise is simply ignored and its consequences are not waited on. Do I need to mock the Promise I want to wait on or is there also a way to simply wait on the internally used promises in general? – Gegenwind Jun 20 '18 at 14:16
  • 1
    `setImmediate()` won't work unless the promise is resolved right away. So you probably want to mock it. – Jonathan Stray Jun 20 '18 at 21:10
  • For those who have multiple promises to be resolved before assertions: the `tick()` helper function along with async test is cleaner and much more readable than nested blocks. One just has to use several `await tick()` calls (as many as there are promises to resolve) and it will work like a charm. Example use case: when using Promise.all with multiple promises in an array, you will have to use 2 tick calls before your assertions. – Cécile Fecherolle Jul 21 '21 at 14:10
1

There isn't much around needing to somehow wait for the promise to fulfill before ending the test. There are two main ways of doing it from your code that I can see.

  1. independently test that onClick and your promise methods. So check that onClick calls the correct function, but spying on setTextWithPromise, triggering a click and asserting that setTextWithPromise was called. Then you can also get the component instance and call that method which returns the promise you can attach a handler and assert it did the right thing.

  2. expose a callback prop that you can pass in that is called when the promise resolves.

monastic-panic
  • 3,987
  • 1
  • 22
  • 20