React - how do I unit test an API call in Jest?

25,384

Solution 1

You cannot access getApiData because it's a private function inside other function (a closure) and it's not exposed to the global scope. That means global variable does not have property getApiData, and you are getting undefined given instead.

To do this you need to export somehow this function, I would suggest by moving it to different file, but the same should be fine as well. Here's a simple example:

export const API = {
  getData() {
    return fetch('/api').then(res => res.json())
  }
}

Somewhere in your component:

API.getData().then(result => setText(result))

And in test:

var apiFunc = jest.spyOn(API, 'getData').mockImplementationOnce(() => {
    return Promise.resolve({
      json: () => Promise.resolve(fakeUserResponse)
    })
  })

There are other ways to achieve that, but maybe this one would be enough.

And I think there would be one more problem. You are using const text = await getByTestId("ptag"), but getBy* functions from react-testing-library are not asynchronous (they do not return a promise you can wait to resolve), so your test will fail, as you wouldn't wait for a mock request to finish. Instead, try findBy* version of this function that you can await on and make sure promise is resolved.

Solution 2

Don't mock the API library. It's better the stub the server responses instead. If you write a bunch of tests that mock out the API call, you're binding the implementation of your app to your tests. Say you don't want to use fetch() but want to use something like isomorphic-unfetch for a SSR app? Switching over an entire test suite of mocks will be really painful.

Instead, use a server stubbing library like nock or msw. Think of these libraries as JSDOM but for your server. This way you're binding your test suite to the backend rather than the implementation library. Let's rewrite your example to show you what I mean:

import React from 'react';
import nock from 'nock';
import { render, shallow, fireEvent } from '@testing-library/react';

import App from './App';

it('displays user data', async () => {
  const scope = nock('https://yoursite.com')
    .get('/api')
    .once()
    .reply(200, {
      data: 'response',
    });

  var {getByTestId, findByTestId} = render(<App />)
  fireEvent.click(getByTestId("apiCall"))
  expect(await findByTestId("ptag")).toHaveTextContent('response');
})

Check out the blog post I wrote for a deeper dive on the subject, Testing components that make API calls.

Share:
25,384

Related videos on Youtube

byte_baron
Author by

byte_baron

Updated on February 10, 2021

Comments

  • byte_baron
    byte_baron over 3 years

    I have a bunch of API calls that I would like to unit test. As far as I know, unit testing API calls doesn't involve actually making those API calls. As far as I know you would simulate responses of those API calls and then test on the DOM changes however I'm currently struggling to do this. I have the following code:

    App.js

    function App() {
    
      const [text, setText] = useState("");
    
      function getApiData() {
            fetch('/api')
            .then(res => res.json())
            .then((result) => {
              console.log(JSON.stringify(result));
              setText(result); 
            })
          }
    
      return (
        <div className="App">
          {/* <button data-testid="modalButton" onClick={() => modalAlter(true)}>Show modal</button> */}
          <button data-testid="apiCall" onClick={() => getApiData()}>Make API call</button>
          <p data-testid="ptag">{text}</p>
        </div>
      );
    }
    
    export default App;
    

    App.test.js

    it('expect api call to change ptag', async () => {
      const fakeUserResponse = {'data': 'response'};
      var {getByTestId} = render(<App />)
      var apiFunc = jest.spyOn(global, 'getApiData').mockImplementationOnce(() => {
        return Promise.resolve({
          json: () => Promise.resolve(fakeUserResponse)
        })
      })
    
    
      fireEvent.click(getByTestId("apiCall"))
      const text = await getByTestId("ptag")
      expect(text).toHaveTextContent(fakeUserResponse['data'])
    })
    

    I'm trying to mock the result of getApiData() here and then test a DOM change (the p tag changes to the result). The above code gives me the error:

    Cannot spy the getApiData property because it is not a function; undefined given instead

    How do I access that class function?

    EDIT:

    I've adapted the code but I'm still having a bit of trouble:

    App.js

    function App() {
    
      const [text, setText] = useState("");
    
      async function getApiData() {
            let result = await API.apiCall()
            console.log("in react side " + result)
            setText(result['data'])
          }
    
      return (
        <div className="App">
          {/* <button data-testid="modalButton" onClick={() => modalAlter(true)}>Show modal</button> */}
          <button data-testid="apiCall" onClick={() => getApiData()}>Make API call</button>
          <p data-testid="ptag">{text}</p>
        </div>
      );
    }
    
    export default App;
    

    apiController.js

    export const API = {
        apiCall() {
            return fetch('/api')
            .then(res => res.json())
        }
    }
    

    Server.js

    const express = require('express')
    const app = express()
    const https = require('https')
    const port = 5000
    
    app.get('/api', (request, res) => {
        res.json("response")
    })
    
    app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))
    

    App.test.js

    import React from 'react';
    import { render, shallow, fireEvent } from '@testing-library/react';
    import App from './App';
    import {API} from './apiController'
    //import shallow from 'enzyme'
    
    it('api call returns a string', async () => {
      const fakeUserResponse = {'data': 'response'};
      var apiFunc = jest.spyOn(API, 'apiCall').mockImplementationOnce(() => {
        return Promise.resolve({
          json: () => Promise.resolve(fakeUserResponse)
        })
      })
      var {getByTestId, findByTestId} = render(<App />)
      fireEvent.click(getByTestId("apiCall"))
      expect(await findByTestId("ptag")).toHaveTextContent('response');
    })
    

    The error I'm getting is

    expect(element).toHaveTextContent()
    
       Expected element to have text content:
         response
       Received:
    
         14 |   var {getByTestId, findByTestId} = render(<App />)
         15 |   fireEvent.click(getByTestId("apiCall"))
       > 16 |   expect(await findByTestId("ptag")).toHaveTextContent('response');
            |                                      ^
         17 | })
         18 | 
         19 | // it('api call returns a string', async () => {
    
    

    Reusable unit test (hopefully):

        it('api call returns a string', async () => {
          const test1 = {'data': 'response'};
           const test2 = {'data': 'wrong'}
    
          var apiFunc = (response) => jest.spyOn(API, 'apiCall').mockImplementation(() => {
            console.log("the response " + JSON.stringify(response))
            return Promise.resolve(response)
            })
    
          var {getByTestId, findByTestId} = render(<App />)
    
          let a = await apiFunc(test1);
          fireEvent.click(getByTestId("apiCall"))
          expect(await findByTestId("ptag")).toHaveTextContent('response');
          let b = await apiFunc(test2);
          fireEvent.click(getByTestId("apiCall"))
          expect(await findByTestId("ptag")).toHaveTextContent('wrong');
    
        })
    
    
  • byte_baron
    byte_baron about 4 years
    Thanks for the help. I've edited the example accordingly however I'm having a bit of trouble. The ptag doesn't seem to receive any textual output.
  • jjanczyk
    jjanczyk about 4 years
    Your spy is incorrect - you are mocking json() method, but it'snot this level (with this mock you are removing a call to json() entirely). Please use this version instead: jest.spyOn(API, "apiCall").mockImplementation(() => Promise.resolve(fakeUserResponse));
  • byte_baron
    byte_baron about 4 years
    Thanks a lot for the help, you've helped me out massively. Do you know what a good way to try this out with multiple fakeUserResponse test cases would be?
  • jjanczyk
    jjanczyk about 4 years
    No problem, glad I could help :)
  • byte_baron
    byte_baron about 4 years
    Sorry to pester you again but do you know what a good way to try this out with multiple fakeUserResponse test cases would be?
  • jjanczyk
    jjanczyk about 4 years
    You should be able to chain mockImplementationOnce: jestjs.io/docs/en/…. Or use mockImplementation and add some conditions inside handler to return different value with following calls