When we talk about async code in the component, usually we would be making API calls using some http library like axios or fetch. You can also use redux and have different types of async code (where you send action to make http call and get the response data in componentDidUpdate as props). We will look into testing redux in other blog posts (link to be updated).

Let's take the example of the below React component which using axios and fetch to make the http call on click of the button. The response data is used to update the state which is displayed as a list. Here is the code of the React component.

import React, { Component } from "react";
import { map } from "lodash";
import axios from "axios";
class AsyncTests extends Component {
  constructor() {
    super();
    this.asyncFunction = this.asyncFunction.bind(this);
    this.axiosFn = this.axiosFn.bind(this);
    this.state = {
      data: []
    };
  }
  axiosFn() {
    axios({
      url: "http://google.com/somedata.json"
    })
      .then(response => {
        this.setState({
          data: response.data
        });
      })
      .catch(e => {
        console.log("error", e);
      });
  }
  asyncFunction() {
    fetch("http://google.com/somedata.json")
      .then(data => {
        this.setState({
          data: data
        });
      })
      .catch(error => {
        this.setState({
          data: error
        });
      });
  }
  render() {
    const { data } = this.state;
    return (
      <div className="App">
        <List data={data} />
        <button id="async" onClick={this.asyncFunction}>
          Async Function
        </button>
      </div>
    );
  }
}
export const List = props => {
  const { data } = props;
  return map(data, d => <span className="index">{d}</span>);
};
export default AsyncTests;

In the above component, the asyncFunction function is called when the button is clicked, and it makes the http call using fetch library. There is another function called axiosFn which makes http call using axios library. When we get the response (success) or the error, the state is set. The List component gets the data as props from the parent component AsyncTests. Initially, the data is empty, and it is set after the response is returned on click of the button.

Here is the unit test for the above component AsyncTests.

import React from "react";
import ReactDOM from "react-dom";
import { shallow } from "enzyme";
import waitUntil from "async-wait-until";
import MockAdapter from "axios-mock-adapter";
import axios from "axios";
import _ from "lodash";
import AsyncTests from "../AsyncTests";
import { List } from "../AsyncTests";
describe("AsyncTests", () => {
  it("renders without crashing", () => {
    const div = document.createElement("div");
    ReactDOM.render(<AsyncTests />, div);
    ReactDOM.unmountComponentAtNode(div);
  });
  it("should handle async function - success", async done => {
    global.fetch = jest
      .fn()
      .mockImplementation(() => Promise.resolve({ name: "abc" }));
    const app = shallow(<AsyncTests />);
    app.setState({
      data: []
    });
    app.instance().asyncFunction();
    await waitUntil(() => {
      return !_.isEmpty(app.state("data"));
    });
    expect(app.state("data")).toEqual({ name: "abc" });
    done();
  });
  it("should handle async function - error", async done => {
    global.fetch = jest
      .fn()
      .mockImplementation(() =>
        Promise.reject({ error: "There was some error" })
      );
    const app = shallow(<AsyncTests />);
    app.setState({
      data: []
    });
    app.instance().asyncFunction();
    await waitUntil(() => {
      return !_.isEmpty(app.state("data"));
    });
    expect(app.state("data")).toEqual({ error: "There was some error" });
    done();
  });
  it("should handle async function - success wo async await", done => {
    global.fetch = jest
      .fn()
      .mockImplementation(() => Promise.resolve({ name: "abc" }));
    const app = shallow(<AsyncTests />);
    app.setState({
      data: []
    });
    app.instance().asyncFunction();
    waitUntil(() => {
      return !_.isEmpty(app.state("data"));
    }).then(() => {
      expect(app.state("data")).toEqual({ name: "abc" });
      done();
    });
  });
  it("should handle axios function - success", async done => {
    const mock = new MockAdapter(axios);
    mock.onGet(/.*/g).reply(200, {
      name: "abc"
    });
    const app = shallow(<AsyncTests />);
    app.setState({
      data: []
    });
    app.instance().axiosFn();
    await waitUntil(() => {
      return !_.isEmpty(app.state("data"));
    });
    expect(app.state("data")).toEqual({ name: "abc" });
    done();
  });
  it("should render the list", async done => {
    const mock = new MockAdapter(axios);
    mock.onGet(/.*/g).reply(200, [1, 2, 3]);
    const app = shallow(<AsyncTests />);
    app.setState({
      data: []
    });
    app.instance().axiosFn();
    await waitUntil(() => {
      return !_.isEmpty(app.state("data"));
    });
    expect(app.state("data")).toEqual([1, 2, 3]);
    app.instance().forceUpdate();
    expect(app.find(List)).toHaveLength(1);
    done();
  });
});

Conclusion

Following things can be noted from the above unit test code

We need to mock the API call to test the success and error scenarios and avoid making actual calls during tests. The fetch library calls for the success response can be mocked using the jest code.

global.fetch = jest
      .fn()
      .mockImplementation(() => Promise.resolve({ name: "abc" }));
  • The fetch library call for error response can be mocked using the jest code
global.fetch = jest
      .fn()
      .mockImplementation(() =>
        Promise.reject({ error: "There was some error" })
      );

The axios library call for the success response can be mocked using the jest code

const mock = new MockAdapter(axios);
    mock.onGet(/.*/g).reply(200, {
      name: "abc"
    });

The axios library call for the error response can be mocked using the jest code. Using non-200 code for the error response (like 400 for bad request).

const mock = new MockAdapter(axios);
    mock.onGet(/.*/g).reply(400, {
      name: "abc"
    });

Because the state data is set in the next event loop, you have to wait for it to be set and to be tested. We are using a library called async-wait-until which returns a promise and checks for a variable. We are using async-wait-until to wait for the state variable to be set. It can be tested using es6 feature async-await using the below code.

it("...", async done => {
    await waitUntil(() => {
      return !_.isEmpty(app.state("data"));
    });
  });

Or if you do not want to use async and await, you can do so as follows.

waitUntil(() => {
      return !_.isEmpty(app.state("data"));
    }).then(() => {
      expect(app.state("data")).toEqual({ name: "abc" });
      done();
    });