React - how do I unit test an API call in Jest?
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.
Related videos on Youtube
byte_baron
Updated on February 10, 2021Comments
-
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 about 4 yearsThanks 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 about 4 yearsYour
spy
is incorrect - you are mockingjson()
method, but it'snot this level (with this mock you are removing a call tojson()
entirely). Please use this version instead:jest.spyOn(API, "apiCall").mockImplementation(() => Promise.resolve(fakeUserResponse));
-
byte_baron about 4 yearsThanks 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 about 4 yearsNo problem, glad I could help :)
-
byte_baron about 4 yearsSorry to pester you again but do you know what a good way to try this out with multiple fakeUserResponse test cases would be?
-
jjanczyk about 4 yearsYou should be able to chain
mockImplementationOnce
: jestjs.io/docs/en/…. Or usemockImplementation
and add some conditions inside handler to return different value with following calls