2

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

  • If it is successful, it increments the counter
  • If it is unsuccessful, it decrements the counter
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!

2 Answers 2

1

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

Sign up to request clarification or add additional context in comments.

2 Comments

Interesting, thanks for that. I'm finding that the .catch() in the test actually ends up firing before the .catch() in the actual react component. So it still ends up testing the DOM before it has a chance to be updated by the component. Is there a good way to make sure the component's promise completes before checking it in the test? Thanks!
@user2490003 I updated my answer, the test can catch the promise and add a then to make sure tests execute after component catch is finished.
0

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"
    );
  });
});

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.