1
votes

I am trying to test a async/await function which does an api call using axios to get users. I am new to testing React applications and using JEST (trying first time), unable to get the test running.

I have tried using mock function in JEST. My Code is as follows:

  // component --users

  export default class Users extends React.Component {
      constructor(props) {
        super(props);
        this.state = {};
        this.getUsers = this.getUsers.bind(this);
       }

  /*does an api request to get the users*/
  /*async await used to handle the asynchronous behaviour*/
  async getUsers() {
    // Promise is resolved and value is inside of the resp const.
     try {
      const resp = await axios.get(
        "https://jsonplaceholder.typicode.com/users"
      );

      if (resp.status === 200) {
        const users = resp.data;
        /* mapped user data to just get id and username*/
        const userdata = users.map(user => {
          var userObj = {};
          userObj["id"] = user.id;
          userObj["username"] = user.username;
          return userObj;
        });
        return userdata;
      }
    } catch (err) {
      console.error(err);
    }
  }


  componentDidMount() {
    this.getUsers().then(users => this.setState({ users: users }));
  }

  /*****************************************************************/
  //props(userid ,username) are passed so that api call is made for
  //getting posts of s psrticular user .
  /*****************************************************************/
  render() {
    if (!this.state.users) {
      return (
        <div className="usersWrapper">
          <img className="loading" src="/loading.gif" alt="Loading.." />
        </div>
      );
    }

    return (
      <div className="usersWrapper">

        {this.state.users.map(user => (
          <div key={user.id}>
            <Posts username={user.username} userid={user.id} />
          </div>
        ))}
      </div>
    );
  }
}


//axios.js-mockaxios
export default {
    get: jest.fn(() => Promise.resolve({ data: {} }))
};

//users.test.js


describe("Users", () => {
  describe("componentDidMount", () => {
    it("sets the state componentDidMount", async () => {
      mockAxios.get.mockImplementationOnce(
        () =>
          Promise.resolve({
            users: [
              {
                id: 1,
                username: "Bret"
              }
            ]
          }) //promise
      );

      const renderedComponent = await shallow(<Users />);
      await renderedComponent.update();
      expect(renderedComponent.find("users").length).toEqual(1);
    });
  });
});

the test fails -

FAIL src/components/users.test.js (7.437s) ● Users › componentDidMount › sets the state componentDidMount

expect(received).toEqual(expected)

Expected value to equal: 1 Received: 0

Please help me figure out the problem. i am totally new to testing reactapps

2
The code in your question does not show how getUsers is called, and does not show your test assertion. Is there a componentDidMount method that you left out? Or does your test call getUsers directly? What happens if you remove the try/catch?Jesse Hallett
getUsers() is called in componentDidMount(). It is there in the posted code.roma
I'm very sorry; I missed the scrollbar somehow.Jesse Hallett
shallow is not an async method, you don't need await before it. It looks like test is finished earlier then component is re-rendered after got users, can you check setTimeout( () => expect(renderedComponent.find("users").length).toEqual(1), 0) (in the very end of your test)?Alex
The solution you gave seems to work. test passed. Thank you.roma

2 Answers

0
votes

It looks like similar to this one.

The problem is that test if finished earlier then async fetchUsers and then setState (it's also async operation). To fix it you can pass done callback to test, and put the last expectation into setTimeout(fn, 0) - so expect will be called after all async operations done:

it("sets the state componentDidMount", (done) => {
  ...
  setTimeout(() => {
    expect(renderedComponent.find("users").length).toEqual(1);
    done();
  }, 0);
});

As mentioned in comment, it's hacky fix, I hope here will be other answers with more jest way to fix it.

0
votes

As far as I understood, what you are trying to do, is to wait for the resolution of a promise, which is 'hidden' inside of your componentDidMount() method.

expect(renderedComponent.find("users").length).toEqual(1);

will not work in my view in this context because find() is trying to select DOM Elements. If you want to check the state, you need to use state():

expect(renderedComponent.state("users").length).toEqual(1);

Still, you will have to find the right way to wait for the resolution of the promise:

To refer to the previous posts and comments, I don't see any effect in using async/await in combination with any of the enzyme methods (update, instance or whatsoever. Enzyme docs also give no hint in that direction, since the promise resolution is not the job of enzyme).

The only robust, clean and (jest-default) way forward is somehow a mixture of the different approaches. Here comes your code slightly changed:

// component --users

export default class Users extends React.Component {
  constructor(props) {
    super(props);
    this.state = {};
    this.getUsers = this.getUsers.bind(this);
  }

  /*does an api request to get the users*/

  /*async await used to handle the asynchronous behaviour*/
  async getUsers() {
    // Promise is resolved and value is inside of the resp const.
    try {
      const resp = await axios.get(
        "https://jsonplaceholder.typicode.com/users"
      );

      if (resp.status === 200) {
        const users = resp.data;
        /* mapped user data to just get id and username*/
        const userdata = users.map(user => {
          var userObj = {};
          userObj["id"] = user.id;
          userObj["username"] = user.username;
          return userObj;
        });
        return userdata;
      }
    } catch (err) {
      console.error(err);
    }
  }


  componentDidMount() {
    // store the promise in a new field, not the state since setState will trigger a rerender
    this.loadingPromise = this.getUsers().then(users => this.setState({users: users}));
  }

  /*****************************************************************/
  //props(userid ,username) are passed so that api call is made for
  //getting posts of s psrticular user .
  /*****************************************************************/
  render() {
    if (!this.state.users) {
      return (
        <div className="usersWrapper">
          <img className="loading" src="/loading.gif" alt="Loading.."/>
        </div>
      );
    }

    return (
      <div className="usersWrapper">

        {this.state.users.map(user => (
          <div key={user.id}>
            <Posts username={user.username} userid={user.id}/>
          </div>
        ))}
      </div>
    );
  }
}


//axios.js-mockaxios
export default {
  get: jest.fn(() => Promise.resolve({data: {}}))
};

//users.test.js
describe("Users", () => {
  describe("componentDidMount", () => {
    it("sets the state componentDidMount",() => {
      mockAxios.get.mockImplementationOnce(
        () =>
          Promise.resolve({
            users: [
              {
                id: 1,
                username: "Bret"
              }
            ]
          }) //promise
      );

      const renderedComponent = shallow(<Users/>);
      //Jest will wait for the returned promise to resolve.
      return renderedComponent.instance().loadingPromise.then(() => {
        expect(renderedComponent.state("users").length).toEqual(1);
      });
    });
  });
});

While doing some research, I came across this post: To be honest I don't know whether it is a good idea, but it works in my async tests, and it avoids the additional reference to the promise in your component. So feel free to try this out also!