Enzyme: Testing the existence of child components that are rendered based on the state of parent components

Created on 7 Mar 2018  路  5Comments  路  Source: enzymejs/enzyme

React: 16.2.0
Enzyme: 3.3.0
Enzyme-adapter-react-16

I have a parent component that renders a list of child components, but it's based on the result I got from an API call, set the state of the parent component, and render the child components using this.state. The problem I have right now is that since setState is an async function, by the time the parent component is mounted using Enzyme, the setState hasn't completed yet and therefore the child components don't exist in the DOM. I just want to know what is the correct way to test the existence of child components in this case?

Any help would be appreciated!

ParentComponent.js

componentDidMount() {
  someAsyncAPICall().then(status => {
    this.setState({status: status});
  });
}

render() {
  return (
    {this.state.status.map((object, rowIndex) =>
      <ChildComponent key={object.id}
                      ...
      />
    )}
  );
}

Mocha unit test for parent

import React from 'react';
import {expect} from 'chai';
import {mount} from 'enzyme';
import fetchMock from 'fetch-mock';
...

describe('ParentComponent', () => {
  const baseURL = "http://localhost:8000";
  const status = [{
    ...
  }];
  before(() => {
    fetchMock.get(`${baseURL}/api`, status);
  });

  after(() => {
    fetchMock.reset();
    fetchMock.restore();
  });

  it('contains the correct number of components', (done) => {
    const wrapper = mount(<ParentComponent />);

    //This is not working - the length of ChildComponent is always 0
    expect(wrapper.find(ChildComponent).length).to.equal(status.length);
  });
});
question

Most helpful comment

I'm not sure the behavior of fetch-mock but you might have to wait for someAsyncAPICall has been resolved and call wrapper.update().

  it('contains the correct number of components', (done) => {
    const wrapper = mount(<ParentComponent />);
    // wait for the API call has benn resolved
    setTimeout() => {
        wrapper.update();
        expect(wrapper.find(ChildComponent).length).to.equal(status.length);
        done();
    });
  });

You might not have to call setTimeout if fetch-mock resolves a fetch call synchronously.

All 5 comments

I'm not sure the behavior of fetch-mock but you might have to wait for someAsyncAPICall has been resolved and call wrapper.update().

  it('contains the correct number of components', (done) => {
    const wrapper = mount(<ParentComponent />);
    // wait for the API call has benn resolved
    setTimeout() => {
        wrapper.update();
        expect(wrapper.find(ChildComponent).length).to.equal(status.length);
        done();
    });
  });

You might not have to call setTimeout if fetch-mock resolves a fetch call synchronously.

I ran into the same problem and solved it by creating a function I can call in the test that pauses until the fetch() calls have finished:

import testhelper from './testhelper.js';

it('example', async () => {
    const wrapper = mount(<MyComponent/>);

    // Wait until all fetch() calls have completed
    await testhelper.waitForFetch(fetchMock);

    expect(wrapper.html()).toMatchSnapshot();
});

The function is defined as:

// testhelper.js

class testhelper
{
    static async waitUntil(fnWait) {
        return new Promise((resolve, reject) => {
            let count = 0;
            function check() {
                if (++count > 20) {
                    reject(new TypeError('Timeout waiting for fetch call to begin'));
                    return;
                }
                if (fnWait()) resolve();
                setTimeout(check, 10);
            }
            check();
        });
    }

    static async waitForFetch(fetchMock)
    {
        // Wait until at least one fetch() call has started.
        await this.waitUntil(() => fetchMock.called());

        // Wait until active fetch calls have completed.
        await fetchMock.flush();
    }
}

export default testhelper;

It seems to work for me, however I'm not sure if there are any race conditions - particularly if fetch() returns and triggers a call to setState(), but the test then continues before the state has been fully set.

@adam-nielsen's approach definitely risks race conditions.

There's no good solution here - typically you'd mock out the API call with a promise you create in your tests - then, you can invoke the async action, await that promise, and afterwards, make your assertions.

There are definitely some race conditions here - two come to mind:

  • API call completes, promise resolves, test assertions run, but the implementation issues a second API call using data from the first, which runs after the test assertions have finished.
  • API call completes, calls setState, test assertions run, then React updates the state.

I am not sure of any good ideas to handle either of these two scenarios, as there don't appear to be any good ways to confirm that functions like componentDidMount() have completed, or that React has finished processing all state changes.

If there was a way to wait for both these things before running the test assertions then it seems like it would solve the race conditions.

Given that there's no good way to do it, that means you have to mock out, or spy on, any async things that you do manually - and manually make your tests wait on those results.

This may change in React 17 and later, but in <= 16, I don't think there's any better solution.

Was this page helpful?
0 / 5 - 0 ratings