10
votes

I am trying to test a React component that runs some asynchronous code and calls setState in componentDidMount.

Here is my react component that I want to test:

/**
*
* AsyncComponent
*
*/

import React from 'react';

class AsyncComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      loaded: false,
      component: null,
    };
  }
  componentDidMount() {
    this.props.component.then(component => {
       this.setState({
         loaded: true,
         component: component.default ? component.default : component,
       });
    });
  }
  render() {
    if (!this.state.loaded) {
      return null;
    }

    const Component = this.state.component;

    const { component, ...rest } = this.props;

    return <Component {...rest} />;
  }
}

export default AsyncComponent;

Here is the test case. I am using jest and enzyme.

import React from 'react';
import { mount } from 'enzyme';

import AsyncComponent from '../index';

const TestComponent = () => <div>Hello</div>;

describe('<AsyncComponent />', () => {
  it('Should render loaded component.', () => {
    const promise = Promise.resolve(TestComponent);
    const rendered = mount(<AsyncComponent component={promise} />);
    expect(rendered.state().loaded).toBe(true);
  });
});

The test fails because state.loaded is still set to false. Is there a way I can make sure that AsyncComponent has fully loaded before calling expect?

I can get it to work if I wrap the expect assertion in a setTimeout, but that seems like a rather hacky way to do it. How should I go about doing this?

3

3 Answers

0
votes

I encountered the same problem, and I came up with some clumsy solution, I have some function call in componentDidMount and I wanted to check if that function has been called, so that code worked for me

const loadFiltersTree = jest.fn()   
const wrapper = shallow(<FilterTree loadFiltersTree={loadFiltersTree} />)
jest.useFakeTimers()
jest.runAllTimers()
setImmediate(() => {
  expect(loadFiltersTree.mock.calls.length).toEqual(1)
})
0
votes

Breaking a promise chain is a common antipattern. As a rule of thumb, a function that uses promises should return a resulting promise to chain, unless this causes a problem. This guarantees that there won't be race conditions when caller function chains a promise. One of reasons for this is improved testability. This also applies to lifecycle hooks like componentDidMount:

componentDidMount() {
  return this.props.component.then(...)
}

Asynchronous Jest test should chain all promises in use and return a promise. async..await is a practical way to do this.

In Enzyme, shallow rendering allows to disable automatic componentDidMount call and chain a promise that lifecycle hook returns:

const wrapper = shallowMount(<AsyncComponent component={promise} />,
  { disableLifecycleMethods: true });
await wrapper.instance().componentDidMount();
expect(wrapper.state().loaded).toBe(true);

This can also be done with full rendering by spying on componentDidMount:

jest.spyOn(AsyncComponent.prototype, 'componentDidMount');
const wrapper = mount(<AsyncComponent component={promise} />);
expect(wrapper.instance().componentDidMount).toHaveBeenCalledTimes(1);
await wrapper.instance().componentDidMount.mock.results[0].value;
expect(wrapper.state().loaded).toBe(true);
-1
votes

You need to notify jest about the promise either by using async/await or return the promise from the test, have a look at the docs

describe('<AsyncComponent />', () => {
  it('Should render loaded component.', async() => {
    const promise = Promise.resolve(TestComponent);
    const rendered = mount(<AsyncComponent component={promise} />);
    await promise
    expect(rendered.state().loaded).toBe(true);
  });
});