Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Enzyme - Test `componentDidMount` Async Call

everyone.

I'm having weird issues with testing a state update after an async call happening in componentDidMount.

Here's my component code:

'use strict';


import React from 'react';
import UserComponent from './userComponent';
const request = require('request');


class UsersListComponent extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      usersList: []
    };
  }

  componentDidMount() {
    request('https://api.github.com/users', (err, res) => {
      if (!err && res.statusCode === 200) {
        this.setState({
          usersList: res.slice(0)
        });
      }
      else {
        console.log(err);
      }
    });
  }

  render() {
    if (!this.state.usersList.length) {
      return null;
    }

    return (
      <div className="users-list">
        { this._constructUsersList() }
      </div>
    );
  }

  _constructUsersList() {
    return this.state.usersList.map((user, index) => {
      return (
        <UserComponent
              key={ index }
              name={ user.name }
              age={ user.age } />
      );
    });
  }
};


export default UsersListComponent;

Now, what I'm doing in my test files (I have a setup comprised of Mocha + Chai + Sinon, all working):

import React from 'react';
import { expect } from 'chai';
import { shallow, mount, render } from 'enzyme';
import sinon from 'sinon';
import UsersListComponent from '../src/usersListComponent';


describe('Test suite for UsersListComponent', () => {
  it('Correctly updates the state after AJAX call in `componentDidMount` was made', () => {
    const server = sinon.fakeServer.create();
    server.respondWith('GET', 'https://api.github.com/users', [
      200,
      {
        'Content-Type': 'application/json',
        'Content-Length': 2
      },
      '[{ "name": "Reign", "age": 26 }]'
    ]);
    let wrapper = mount(<UsersListComponent />);
    server.respond();
    server.restore();
    expect(wrapper.update().state().usersList).to.be.instanceof(Array);
    console.log(wrapper.update().state().usersList.length);
  });
});

State does not get updated, even though I call update() on wrapper. Length is still 0. Am I missing something here? Do I need to mock the server response in another way?

Thnx for the help!

like image 902
r31gN_ Avatar asked Jul 11 '16 13:07

r31gN_


People also ask

Can we use async in componentDidMount?

Used mostly for data fetching and other initialization stuff componentDidMount is a nice place for async/await in React.

How do you test async function in jest enzyme?

You just add the “async” keyword as the name of the function test, and the “await” keyword in front of the function that calls the endpoint. Using these two keywords lets Jest know that the test will be an async one. If you expect the promise to be rejected, you must use the catch method, as you did for Promises.

How do you write test cases for componentDidMount in react?

What to do when you want to test that a function is been called, on componentDidMount() React lifecycle Method. Basically the component code looks like this: state = { randomStateToPopulate: [] }; // Test componentDidMount componentDidMount() { this. randomFunction(); } randomFunction= () => { listRandomData().


2 Answers

Give you my solution:

  • enzyme 3.9.0
  • enzyme-adapter-react-16 1.9.1
  • react 16.8.0
  • jest

1- Mock data service

You must mock the data service (usually something like axios or an other HTTP library).

File: __mocks__/LogDataService.js:

let searchData = {}

const LogDataService = {
    search: () => new Promise((resolve, reject) =>
        process.nextTick(() => resolve(searchData))
    ),
    setSearchData: (data) => searchData = data
}

export default LogDataService

2- Tests

File: __tests__/toto.test.js:

jest.mock('services/LogDataService')

import LogDataService from 'services/LogDataService';

// Declare a super promise that will synchro all promises
function flushPromises() {
    return new Promise(resolve => setImmediate(resolve));
}

it('renders without data', async () => {
    // Set dummy data
    LogDataService.setSearchData([])
    // The component we want to test that use
    // LogDataService.search method we just mock up
    const component = mount(<JobsExecutionTimeline />);
    // wait
    await flushPromises();
    // re-build the render
    component.update()
    // Compare to snapshot
    expect(toJson(component)).toMatchSnapshot();
});
like image 120
Thomas Decaux Avatar answered Nov 01 '22 05:11

Thomas Decaux


You can abstract the user list retrieval away from the react component via passing a function that returns a Promise so that instead of

  componentDidMount() {
    request('https://api.github.com/users', (err, res) => {
      if (!err && res.statusCode === 200) {
        this.setState({
          usersList: res.slice(0)
        });
      }
      else {
        console.log(err);
      }
    });
  }

Replace it with

  componentDidMount() {
     var comp = this;
     this.props.getUsers()
        .then(function(usersList) {
          comp.setState({
            usersList: usersList
          });
        })
        .catch(function (err) {
            console.log(err);
        });
  }

And inside your test mock that getUsers function:

    it('Correctly updates the state after AJAX call in `componentDidMount` was made', (done) => {

      let resolveGetUsers;

      let getUsers = function() {
        return new Promise(function (resolve, reject) {
                  resolveGetUsers = resolve;
                });
      }

      let wrapper = mount(<UsersListComponent getUsers={getUsers} />);

      resolveGetUsers([{ "name": "Reign", "age": 26 }]);


      // promise resolve happens in a subsequent event loop turn so move assertions inside setImmediate
      setImmediate(() => {

        expect(wrapper.update().state().usersList).to.be.instanceof(Array);
        ...

        done();
      });
    }

Note that I have done this and it works for me (even without the wrapper.update() part) and here I tried to apply it to your code example without running it..

Also note that it should work in cases other then componentDidMount too - like having an async action triggered after clicking a button for example..

like image 37
bodrin Avatar answered Nov 01 '22 06:11

bodrin