smitp33
smitp33

Reputation: 421

Testing React with Jest issue with async call

I have a simple React Parent component that has a single state value called counter. Under the Parent component I then have a single Child component that is a button with an onClick hander that calls back to the Parent to update the state.

import React, { Component } from 'react';
import axios from 'axios'

class Parent extends Component {
  state = {counter: 0}

  onClick = () => {
    console.log("Button clicked")
    this.setState(prevState => ({
      counter: prevState.counter + 1
    }))
  }

  render() {
    return (
      <div>
        <Child onClick={this.onClick} action="Click"/>
      </div>
    );
  }
}

class Child extends Component {

  render() {
    return (
      <button onClick={this.props.onClick}>{this.props.action}</button>
    )
  }
}

export default Parent

This tests fine using Jest with the following test:

import React from 'react';
import {mount, shallow} from 'enzyme';
import Parent from '../Problem';

it('handles Click event', () => {

  const wrapper = mount(<Parent />)
  expect(wrapper.state().counter).toEqual(0)
  console.log(wrapper.debug())
  const button = wrapper.find('button').at(0);
  button.simulate('click');
  expect(wrapper.state().counter).toEqual(1)
  console.log(wrapper.state().counter)

});

If I change the code to the following to simulate an async update:

import React, { Component } from 'react';
import axios from 'axios'

class Parent extends Component {
  state = {counter: 0}

  onClickAsync = () => {
    console.log("Async Button clicked")
    axios.get(`https://api.github.com/users/smitp33`)
      .then(resp => {
          this.setState(prevState => ({
            counter: prevState.counter + 1
          }))
        }
      )
  }

  render() {
    return (
      <div>
        <Child onClick={this.onClickAsync} action="Click"/>
      </div>
    );
  }
}

class Child extends Component {

  render() {
    return (
      <button onClick={this.props.onClick}>{this.props.action}</button>
    )
  }
}

export default Parent

... then the same test fails as it does not appear that the counter has been updated.

handles Click event

    expect(received).toEqual(expected)

    Expected value to equal:
      1
    Received:
      0

      at Object.<anonymous>.it (src/__tests__/Problem.test.js:14:35)
      at Promise.resolve.then.el (node_modules/p-map/index.js:46:16)
      at process._tickCallback (internal/process/next_tick.js:109:7)

I have added a callback to the setState function to prove that the state has indeed changed and it definitely has, so I believe the issue is around the Jest test simply completing before the async component call has completed.

I have read various articles and the Jest docs and tried all sorts of things here to try and get the test to pass, but have not succeeded thus far, so I thought I would post here to see if anyone could provide some clear advice on how exactly this should be approached.

Details on exact dependancies are as follows:

"dependencies": {
    "axios": "^0.17.0",
    "enzyme": "^3.1.0",
    "enzyme-adapter-react-16": "^1.0.2",
    "jest-enzyme": "^4.0.1",
    "prop-types": "^15.6.0",
    "react": "^16.0.0",
    "react-circular-progressbar": "^0.5.0",
    "react-dom": "^16.0.0",
    "react-percentage-circle": "^1.1.3",
    "react-scripts": "1.0.15"
  }

This is all done in a vanilla 'create-react-app' setup with the following defined in the setupTests.js:

import Enzyme from "enzyme";
import 'jest-enzyme';
import Adapter from "enzyme-adapter-react-16";

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

global.requestAnimationFrame = function(callback) {
  setTimeout(callback, 0);
}; 

Upvotes: 0

Views: 1487

Answers (1)

smitp33
smitp33

Reputation: 421

With the details provided above I managed to get this to work as follows:

Instead of testing the async click as part of a simulation I chose to test the function in isolation.

By modifying the click event to actually return something ... in this case a Promise I was then able to make proper use of async and await in the test.

Modified Code:

  onClickAsync = () => {
    console.log("Async Button clicked")
    return (
        axios.get(`https://api.github.com/users/smitp33`)
          .then(resp => {
              this.setState(prevState => ({
                counter: prevState.counter + 1
              }))
            }
          )
    )
  }

Modified test:

test('calls Async Click function', async () => {
  const wrapper = mount(<Parent />)
  console.log(wrapper.state().counter)
  await wrapper.instance().onClickAsync()
  console.log(wrapper.state().counter)
  expect(wrapper.state().counter).toEqual(1)
});

Upvotes: 0

Related Questions