10

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.

2 Answers 2

15

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);
  });
Sign up to request clarification or add additional context in comments.

Comments

-1

Reference: Testing with React's Jest and Enzyme when simulated clicks call a function that calls a promise

Wrap your expect statement in setImmediate

setImmediate(() => {
    expect(spotifyMock.getUserInfo).toHaveBeenCalledTimes(1);
})

1 Comment

According to the doc you linked, setImmediate() is non-standard.

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.