Vincent Zhang

Engineering, Design & Productivity

Testing React component state changes in async promises with Jest, Enzyme and Snapshots

Somewhere in your React class component, there is a promise call. Depending on the result of the promise, your component’s state changes, thus causing a change in the rendered output.

You’d like to test this entire flow with Jest, Enzyme and the principles of snapshot testing. How should we proceed?

The answer is it depends. The exact approach will be depending on how the promise is introduced and invoked.

Before we start

Please read Jest’s official documentation: An Async Example. It contains code samples and explanations on the basics of testing all asynchronous code, including Promises. This article serves as a supplement of it.

I use Enzyme to help me with generating component snapshots for this article. It’s not required, you can achieve the same result with just Jest, or with other React testing utilities. The key takeaway will be the methodologies and concepts behind this article.

Promise is a class member function

class YourClassComponent extends React.Component {
  ...
  classFunction() {
    return new Promise((resolve, reject) => {
      resolve('data');
    });
  }
}

Let us start with the simplest case. We have a promise that is directly returned by classFunction, a class member function (or class member method) of YourClassComponent.

For now, let us keep it really simple and not worry about the state changes inside the promise resolve.

You can directly stub, spy or inspect this function from your test cases, once you create an instance of your component:

const wrapper = shallow(<YourClassComponent />);
wrapper.instance().classFunction(); // access the promise here

Promise is a function from another module

import makePromise from './someHelper';

class YourClassComponent extends React.Component {
  ...
  classFunction() {
    makePromise().then(data => {
      setState({ mood: 'happy' });
    });
  }
}

This is a more complicated (and typical) promise example. Now the classFunction no longer returns a promise directly; instead, it is invoking a promise function imported from another module outside of YourClassComponent.

Let’s make it more interesting by having a state change in the success route of the promise. How should we approach its test?

Testing promises with state changes

A common pattern when people writing tests for this situation is to directly expect the state variable to be a certain value, like so:

const wrapper = shallow(<YourClassComponent />);
// ...mock the promise here
expect(wrapper.state('mood')).toEqual('happy');

While this would let your test pass, I believe this goes against the idea of snapshot testing, especially if YourClassComponent is reflecting a frontend component that involves user interaction. It’s better to let the component lifecycle methods take care of it and inspect the final status of the component. For more thorough essay on this topic please read it here.

It’s better to mock the imported function, then use async / await on it, and let the component update itself as the code implies. After the await, the promise should have resolved and the state has been updated.

Then we can update the wrapper and generate the snapshot as expected.

In this scenario, if you do not use async / await, the .then() will always execute after the test code completed. Because the promise is considered asynchronous, even if it contains no network requests, it will be executed after the sequential test code completed.

test('promise with state change', async () => {
  const mockMakePromise = jest.fn(() => return Promise.resolve({}));
  jest.mock('./someHelper', () => {
    makePromise: mockMakePromise
  });
  const wrapper = shallow(<MyClassComponent />);
  await makePromise; // a reference to the promise in component code
  wrapper.update();
  expect(wrapper).toMatchSnapshot();
});

Testing promises with state changes, without a direct reference

Let’s make it even harder. What if you do not have the luxury of a direct reference to the promise function in YourClassComponent?

I encountered this situation recently when I was working with Google API; its functions are attached to the window object; they do not belong to the class component and has no import statements.

class YourClassComponent extends React.Component {
  ...
  classFunction() {
    window.gapi.client.load().then(data => {
      setState({ mood: 'happy' });
    });
  }
}

How can we make our testing life easier by introducing a minimum change to the component code?

In this situation, I would recommend assigning the promise to a class property (class variable) promiseHolder. We make our own reference that’ll be vital in the tests.

class YourClassComponent extends React.Component {
  constructor() {
    this.promiseHolder;
  }
  ...
  classFunction() {
    this.promiseHolder = window.gapi.client.load().then(data => {
      setState({ mood: 'happy' });
    });
  }
}

In the tests, now we have a reachable reference we can apply async / await onto. The rest of the test is similar to cases above.

beforeEach(() => {
  global.window.gapi.client.load ={ ... };
});

test('promise with state change', async () => {
  const wrapper = shallow(<MyClassComponent />);
  await wrapper.instance().promiseHolder;
  // the wrapper state is updated after this line
  wrapper.update();
  expect(wrapper).toMatchSnapshot();
});

Alternative testing approach: process.nextTick

Another approach worth considering is process.nextTick. It’s a node utility that adds the passed callback to the nextTick queue. The nextTick queue is run after all events in the current eventLoop have finished. See this article for more information.

Leave a comment