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');
})
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.