简体   繁体   中英

How do I test async fetch in react componentDidMount with chained promises?

I have been trying to understand how to test a mounted component that runs async fetch during componentDidMount .

The issue is that I can make it wait for the initial fetch to trigger, but not wait to resolve all of the chain from the promise.

Here is an example:

import React from "react";

class App extends React.Component {
  state = {
    groceries: [],
    errorStatus: ""
  };

  componentDidMount() {
    console.log("calling fetch");

    fetch("/api/v1/groceries")
      .then(this.checkStatus)
      .then(this.parseJSON)
      .then(this.setStateFromData)
      .catch(this.setError);
  }

  checkStatus = results => {
    if (results.status >= 400) {
      console.log("bad status");

      throw new Error("Bad Status");
    }

    return results;
  };

  setError = () => {
    console.log("error thrown");

    return this.setState({ errorStatus: "Error fetching groceries" });
  };

  parseJSON = results => {
    console.log("parse json");

    return results.json();
  };

  setStateFromData = data => {
    console.log("setting state");

    return this.setState({ groceries: data.groceries });
  };

  render() {
    const { groceries } = this.state;

    return (
      <div id="app">
        {groceries.map(grocery => {
          return <div key={grocery.id}>{grocery.item}</div>;
        })}
      </div>
    );
  }
}

export default App;

Test:

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import React from 'react';
import { mount } from 'enzyme'
import App from './App';

Enzyme.configure({ adapter: new Adapter() });

const mockResponse = (status, statusText, response) => {
  return new window.Response(response, {
    status: status,
    statusText: statusText,
    headers: {
      'Content-type': 'application/json'
    }
  });
};

describe('App', () => {
  describe('componentDidMount', () => {
    it('sets the state componentDidMount', async () => {
      console.log('starting test for 200')

      global.fetch = jest.fn().mockImplementation(() => Promise.resolve(
        mockResponse(
          200,
          null,
          JSON.stringify({
            groceries: [
              { item: 'nuts', id: 10 }, { item: 'greens', id: 3 }
            ]
          })
        )
      ));

      const renderedComponent = await mount(<App />)
      await renderedComponent.update()

      console.log('finished test for 200')
      expect(renderedComponent.state('groceries').length).toEqual(2)
    })

    it('sets the state componentDidMount on error', async () => {
      console.log('starting test for 500')

      window.fetch = jest.fn().mockImplementation(() => Promise.resolve(
        mockResponse(
          400,
          'Test Error',
          JSON.stringify({ status: 400, statusText: 'Test Error!' })
        )
      ))

      const renderedComponent = await mount(<App />)
      await renderedComponent.update()

      console.log('finished test for 500')
      expect(renderedComponent.state('errorStatus')).toEqual('Error fetching groceries')
    })
  })
})

When this runs, I receive this order of console logging (note that the test finishes and then it logs that state was set):

console.log src/App.test.js:22
  starting test for 200

console.log src/App.js:10
  calling fetch

console.log src/App.js:36
  parse json

console.log src/App.test.js:39
  finished test for 200

console.log src/App.js:42
  setting state

I have created an example sandbox of my code:

编辑3xkq4my426

This is abstracted a lot more in my apps, so changing the code itself is much more difficult (For example I want to test at a higher component, which has redux store, and this lower component calls the fetch, and sets the store eventually through a thunk).

How is this tested?

Update method actually not returning promise that's why await is not working properly. To fix unit test you could move fetch call to another method and use that function from your test so that await works properly.

 componentDidMount() {
    console.log("calling fetch");

    this.fetchCall();
  }

  fetchCall() {
    return fetch("/api/v1/groceries")
      .then(this.checkStatus)
      .then(this.parseJSON)
      .then(this.setStateFromData)
      .catch(this.setError);
  }

use instance() to access fetchCall method.

const renderedComponent = mount(<App />);
await renderedComponent.instance().fetchCall();

i have modified above changes in codesandbox: https://codesandbox.io/s/k38m6y89o7

I have no idea why await renderedComponent.update() does not help you here( .update does not return a Promise but it still means everything below comes as separated microtask).

But wrapping things into setTimeout(..., 0) works for me. So it's difference between microtask and macrotask actually happens in some way.

   it("sets the state componentDidMount on error", done => {
      console.log("starting test for 500");

      window.fetch = jest
        .fn()
        .mockImplementation(() =>
          Promise.resolve(
            mockResponse(
              400,
              "Test Error",
              JSON.stringify({ status: 400, statusText: "Test Error!" })
            )
          )
        );

      const renderedComponent = mount(<App />);
      setTimeout(() => {
        renderedComponent.update();

        console.log("finished test for 500");
        expect(renderedComponent.state("errorStatus")).toEqual(
          "Error fetching groceries"
        );
        done();
      }, 0);
    });
  });

The only disadvantage of this approach: when expect() fails it does not display failing message into Jest output. Jest is just complains on test have not finished in 5000 ms. In the same time valid error message like Expected value to equal: ... goes to console.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM