Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test state update and component rerender after async call in react

I am writing an simple app where after clicking the button, async call to spotify API should be performed and when promise resolves it should update component's state. I am using react hooks to manage state in my component.

In my tests I mocked API call.

spotify.jsx

export default class Spotify {
  constructor(token) {
    this.axiosInstance = axios.create({
      baseURL: baseURL,
      headers: buildHeaders(token),
    });
  }

  async getUserInfo() {
    const userInfo = await this.axiosInstance({
      url: `/me`,
    });
    return userInfo.data
  }
}

spotify mock:

const getUserInfoMock = jest.fn();

const mock = jest.fn().mockImplementation(() => ({
  getUserInfo: getUserInfoMock,
}));

export default mock;

User.jsx

const User = props => {
  const [user, setUser] = useState(null);
  const {token} = useContext(AuthContext);
  const spotify = useMemo(() => new Spotify(token), [token]);

  const getUserInfo = async () => {
    console.log("button clicked")
    const fetched = await spotify.getUserInfo();
    console.log(fetched)
    setUser(fetched);
  }

  return (
    <React.Fragment>
      <p>user page</p>
      <button onClick={getUserInfo} > click me </button>
      {user && (
        <div>
          <p>{user.display_name}</p>
          <p>{user.email}</p>
        </div>
      )}
    </React.Fragment>
  );
};

My question is how to properly test such behavior. I managed to make it pass but isn't calling await on simulate() an ugly hack? Simulate does not return a promise. Here is a test:

  it('updates display info with data from api', async () => {
    const userInfo = {
      display_name: 'Bob',
      email: '[email protected]',
    };
    spotifyMock.getUserInfo.mockImplementation(() => Promise.resolve(userInfo));

    wrapper = mount(<User />);
    expect(wrapper.find('p')).toHaveLength(1);
    await wrapper
      .find('button')
      .last()
      .simulate('click');

    wrapper.update();
    expect(wrapper.find('p')).toHaveLength(3);
  });

On the other hand when i check only if mock was called I don't need to use async/await and test passes:

  it('calls spotify api on click', () => {
    wrapper = mount(<User />);
    expect(spotifyMock.getUserInfo).not.toHaveBeenCalled();
    wrapper
      .find('button')
      .last()
      .simulate('click');
    expect(spotifyMock.getUserInfo).toHaveBeenCalledTimes(1);
  });

I wonder if my way of testing is proper and what if I want to add a feature to fetch data from api when component renders - with useEffect hook. Does Enzyme has a full support for react hooks? I also struggle with warning Warning: An update to User inside a test was not wrapped in act(...) even if I wrap a mount and simulate functions.

like image 532
rungus2 Avatar asked Nov 13 '19 19:11

rungus2


1 Answers

You should wrap your render-affecting calls in an async act() function as per Dan Abramov's blog post like so:

  it('calls spotify api on click', async () => {
    await act(async () => {
      wrapper = mount(<User />);
    });
    expect(spotifyMock.getUserInfo).not.toHaveBeenCalled();

    await act(async () => {
      wrapper
        .find('button')
        .last()
        .simulate('click');
    });

    wrapper.update();
    expect(spotifyMock.getUserInfo).toHaveBeenCalledTimes(1);
  });
like image 58
Sebastien De Varennes Avatar answered Oct 25 '22 19:10

Sebastien De Varennes