user2490003
user2490003

Reputation: 11890

Testing async behavior in React with Axios and Jest

Consider the following oversimplified React component. When you click the button, it makes an API call to an external URL.

import axios from 'axios';
import PropTypes from 'prop-types';
import React from 'react';

class MyCoolButton extends React.Component {
  static propTypes = {
    initialCounter: PropTypes.number.isRequired
  };

  constructor(props) { 
    super(props);

    this.onClick = this.onClick.bind(this);

    this.state = {
      counter: props.initialCounter
    }
  }

  onClick() {
    const url = `/some/url/here`;
    const data = { foo: 'bar' };
    const config = { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } };

    const { counter } = this.state;

    return axios.patch(url, data, config)
      .then((response) => { /* Success! */ this.setState({ counter: counter + 1 }); })
      .catch((error) => { /* Failure :( */ this.setState({ counter: counter - 1 }); });
  }

  render() {
    return (
      <div className="container">
        <span>Counter value is: {this.state.counter}</span>
        <input className="cool-button" type="button" onClick={this.onClick} />
      </div>        
    );
  }

}

export default MyCoolButton;

I wanted to write a test case using Jest to ensure that when there is a failure, we are correctly decrementing the button.

I tried the following:

describe('an error occurred while updating', () => {
  beforeEach(() => {
    axios.patch.mockImplementationOnce(() => Promise.reject('boo'));
  });

  it('decrements the counter', async() => {
    // NOTE: The below uses Enzyme and Chai expectation helpers

    wrapper = mount(<MyCoolButton initialCounter={99} />);

    // Click the button
    wrapper.find(`.cool-button`).first().simulate('click');

    // Check for decrmented value
    const body = wrapper.find('.container span');
    expect(body).to.have.text('Counter value is: 98');
  });
});

The problem is that the click and subsequent state update are executed asynchronously, so we check for the failure before it has a chance to even update the component with the failure.

A lot of the examples online seem to suggest async/await which I don't understand that well. It looks like await takes a Promise as an argument, but in my case I'm simulating a click which further calls a handler which returns a Promise, so I can't await on that axios Promise to complete directly.

What's the best practice in testing here?

Thanks!

Upvotes: 2

Views: 253

Answers (2)

HMR
HMR

Reputation: 39250

I think the following will do the trick:

describe('an error occurred while updating', () => {
  beforeEach(() => {});

    it('decrements the counter', async () => {
      const promise = Promise.reject('boo');
      axios.patch.mockImplementationOnce(() => promise);
      const wrapper = mount(
        <MyCoolButton initialCounter={99} />
      );

      // Click the button
      wrapper.find(`.cool-button`).first().simulate('click');
      //do catch().then to make sure test executes after
      //  component caught the rejection.
      return promise.catch(x=>x).then(() => {
        // Check for decrmented value
        const body = wrapper.find('.container span');
        expect(body).to.have.text('Counter value is: 98');
      });
    });
});

Here are some async examples for jest

Upvotes: 1

Callum Hart
Callum Hart

Reputation: 214

You need to mount the component and simulate the click event before making the assertion:

describe("an error occurred while updating", () => {
  let wrapper;

  beforeEach(() => {
    axios.patch.mockRejectedValue("Mock error message");
    wrapper = mount(<MyCoolButton initialCounter={99} />);
    wrapper.find(".cool-button").simulate("click");
  });

  it("decrements the counter", () => {
    expect(wrapper.find(".container span").text()).toEqual(
      "Counter value is: 98"
    );
  });
});

Upvotes: 0

Related Questions