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

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

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.

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.

Related

Mocking a promise inside useEffect with CRA, React Testing Library and Jest

I am having an issue mocking a returned Promise using:
Create React app
Jest
RTL
I have a file:
const books = [{
id: 1,
name: 'book'
}];
export const getBooks = () =>
new Promise((res) => res(books));
I have a useEffect in my app:
export const App = () => {
const [books, setBooks] = useState(undefined);
useEffect(() => {
const fetchData = async () => {
try {
const response = await getBooks();
setBooks(response);
} catch (error) {
setError("There seems to be an issue. Error:", error);
}
};
fetchData();
}, []);
return (
<div>
{books &&
books.map((book) => {
return (
<li key={book.id}>
{book.name}
</li>
);
})
}
</div>
I have a test:
import { App } from './App';
import { getBooks } from './books';
jest.mock('./books', () => ({
getBooks: jest.fn(),
}));
getBlocks.mockReturnValue(() => Promise.resolve([{
id: 1,
name: 'mock book'
}]));
describe('App', () => {
it('should render blocks', async () => {
await render(<App />);
expect(screen.getByText('mock book')).toBeInTheDocument();
});
});
I just can't mock the return value! I can assert it's been called and I can console log the getBooks to see that it's mocked I just can't get any results. I also want to reject it so I can test the unhappy path but it won't work. Any ideas?
Few things:
You have a typo, it's not getBlocks but getBooks.
The await keyword is not necessary before rendering the component with render.
getBooks returns a promise that resolves with the value of books, yet when you're trying to mock it, you are making it return a function that returns a promise. Very different things.
You have to move the mocking to the test block in which it'll be used, or if you need this mocked value from getBooks on each one of your tests, you can move it inside a beforeEach hook. You can always override it for a specific test in which you are testing some edge case (e.g. an exception being thrown by the function, A.K.A "Unhappy path").
On the component's first render, books will be undefined, so you need to wait for the state to be updated. getByText query won't work, since it will immediately throw an error because it won't find the text you're expecting. You need to use the findByText query for this. It returns a promise that resolves when an element that matches the given query is found and rejects if the element is not found after the default timeout of 1000ms.
Since getBooks returns a promise, it makes more sense to use mockResolvedValue instead of mockReturnValue.
import { render, screen } from "#testing-library/react"
import { App } from "./App"
import { getBooks } from "./books"
jest.mock("./books", () => ({
getBooks: jest.fn()
}))
describe("App", () => {
it("should render blocks", async () => {
getBooks.mockResolvedValueOnce([{ id: 1, name: "mock book" }])
render(<App />)
expect(await screen.findByText("mock book")).toBeInTheDocument()
})
})
Try this:
jest.mock('./books', () => ({
getBooks: jest.fn().mockReturnValue(() => Promise.resolve([{
id: 1,
name: 'mock book'
}]));
}));

How can I make my token refresh function more testable?

I have a function in my React app's App component that refreshes the user's access token when it's first rendered (useEffect hook). At the moment, unit tests are checking to see how the state has changed at the end of the component's rendering. How can I make the function itself more testable?
I've considered refactoring to have the dispatch() hook, logout() reducer, and local setLoading() state function passed into the function as arguments so they can be mocked/so the function can be externalized from the component itself, but I'm not sure what value this would bring.
I understand that 100% test coverage is not necessary, but I'm learning and want to do the best I can while I do so.
A little context:
The app uses a ReduxToolkit slice for authentication state, including the user object and access token for the currently authenticated user, or nulls for guest users.
Auto refresh logic is implemented into a custom fetchBaseQuery.
The code below describes refreshing the access token for a user who's logged in and has a refresh token in localStorage, but has refreshed the page, clearing the redux state. It refreshes the accessToken before rendering any routes/views to avoid the user having to enter credentials every time the page refreshes.
Here's the current implementation:
//imports
...
const App = () => {
const dispatch = useAppDispatch();
const [loading, setLoading] = useState(true);
useEffect(() => {
const refresh = async () => {
const token = localStorage.getItem("refreshToken");
if (token) {
const refreshRequest = {
refresh: token,
};
const response = await fetch(
`${process.env.REACT_APP_API_URL}/auth/refresh/`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(refreshRequest),
}
);
if (response.status === 200) { // This branch gets no test coverage and I can't figure out how to fix that.
const data: RefreshResponse = await response.json();
// Should this be passed into the function to make it more reusable/testable?
dispatch(
setCredentials({ user: data.user, token: data.accessToken })
);
}
}
// Should this be passed into the function to make it more reusable/testable?
setLoading(false);
};
refresh();
}, [dispatch]);
if (loading) return (
<div className="h-100 d-flex justify-content-center align-items-center bg-dark">
<Spinner animation="border" />
</div>
);
return (
<>
<Routes>
// Routes
</Routes>
</>
);
}
export default App;
and here are the relevant test cases:
it("should successfully request refresh access token on render", async () => {
// refresh() expects a refreshToken item in localStorage
localStorage.setItem("refreshToken", "testRefreshToken");
// I can't use enzyme because I'm on react 18, so no shallow rendering afaik :/
// renderWithProviders renders including a redux store with auth/api reducers
const { store } = renderWithProviders(
<MemoryRouter>
<App />
</MemoryRouter>
);
await waitFor(() => {
expect(store.getState().auth.token).toBe("testAccessToken");
});
localStorage.removeItem("refreshToken");
});
it("should fail to request refresh access token on render", async () => {
localStorage.setItem("refreshToken", "testRefreshToken");
// msn api route mocking, force a 401 error rather than the default HTTP 200 impl
server.use(
rest.post(
`${process.env.REACT_APP_API_URL}/auth/refresh/`,
(req, res, ctx) => {
return res(ctx.status(401));
}
)
);
const { store } = renderWithProviders(
<MemoryRouter>
<App />
</MemoryRouter>
);
await waitFor(() => {
expect(store.getState().auth.token).toBeNull();
});
localStorage.removeItem("refreshToken");
});
it("should not successfully request refresh access token on render", async () => {
const { store } = renderWithProviders(
<MemoryRouter>
<App />
</MemoryRouter>
);
await waitFor(() => {
expect(store.getState().auth.token).toBe(null);
});
});
My suggestions:
Move dispatch, useState and useEffect to custom hook. It can look like:
const useTockenRefresh() { // Name of the custom hook can be anything that is
started from work 'use'
const dispatch = useAppDispatch();
const [loading, setLoading] = useState(true);
useEffect(() => {
/* useEffect code as is */
}, [/* deps */])
return loading
}
export default useTockenRefresh
Utilize useTockenRefresh in your component
const App = () => {
const loading = useTockenRefresh()
if (loading) return (
// And rest of your code
}
Now it is possible to test only useTockenRefresh in isolation. I would suggest to use React Hooks Testing Library for this purpose. And as this will be Unit Tests, it is better to mock everything external, like useAppDispatch, fetch, etc.
import { renderHook, act } from '#testing-library/react-hooks'
// Path to useTockenRefresh should be correct relative to test file
// This mock mocks default export from useTockenRefresh
jest.mock('./useTockenRefresh', () => jest.fn())
// This mock for the case when useAppDispatch is exported as named export, like
// export const useAppDispatch = () => { ... }
jest.mock('./useAppDispatch', () => ({
useAppDispatch: jext.fn(),
}))
// If fetch is in external npm package
jest.mock('fetch', () => jest.fn())
jest.mock('./setCredentials', () => jest.fn())
// Mock other external actions/libraries here
it("should successfully request refresh access token on render", async () => {
// Mock dispatch. So we will not update real store, but see if dispatch has been called with right arguments
const dispatch = jest.fn()
useAppDispatch.mockReturnValueOnce(dispatch)
const json = jest.fn()
fetch.mockReturnValueOnce(new Promise(resolve => resolve({ status: 200, json, /* and other props */ })
json.mockReturnValueOnce(/* mock what json() should return */)
// Execute hook
await act(async () => {
const { rerender } = renderHook(() => useTockenRefresh())
return rerender()
})
// Check that mocked actions have been called
expect(fetch).toHaveBeenCalledWith(
`${process.env.REACT_APP_API_URL}/auth/refresh/`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(refreshRequest),
})
expect(setCredentials).toHaveBeenCalledWith(/* args of setCredentials from mocked responce object */
// And so on
}

How to correctly test React with act

I'm trying to test a component but it errors with
console.error
Warning: An update to Example inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
at Example (/Users/thr15/Developmemt/Boost/basket-creator/frontend/src/Page/F1/Example.tsx:5:29)
at WrapperComponent (/Users/thr15/Developmemt/Boost/basket-creator/frontend/node_modules/enzyme-adapter-utils/src/createMountWrapper.jsx:49:26)
Here's a simplified version of my component
import {useState} from 'react';
function Example(): JSX.Element {
const [name, setName] = useState('');
const [, setLoading] = useState(false);
const [results, setResults] = useState<number[]>([]);
/**
* Search the baskets.
*/
const search = async () => {
// Let the UI know we're loading
setLoading(true);
// Get the baskets
try {
const baskets: number[] = await (await fetch('/test?name=' + name)).json();
// Give the UI the data
setLoading(false);
setResults(baskets);
} catch (e) {
console.error(e);
}
};
return <div className={"content"}>
<input value={name} onChange={(e) => setName(e.target.value)}/>
<button onClick={search}>Search</button>
{results.length}
</div>
}
export default Example;
and my test so far
import Enzyme, {mount} from 'enzyme';
import Adapter from '#wojtekmaj/enzyme-adapter-react-17';
import Example from "./Example";
Enzyme.configure({adapter: new Adapter()});
describe('Example', () => {
test('searching requests the correct URL', () => {
fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve([{a: 1}, {b: 2}]),
})
);
let searchButton;
const wrapper = mount(<Example/>);
const input = wrapper.find('input').at(0);
searchButton = wrapper.find('button').at(0);
input.simulate('change', {target: {value: 'Driver Name'}});
searchButton.simulate('click');
expect(searchButton.text()).toBe('Search');
expect(fetch.mock.calls.length).toBe(1);
expect(fetch.mock.calls[0][0]).toBe('/test?name=Driver Name');
});
});
I've tried wrapping act around various parts of the test, and it either still errors, or the name isn't appended to the query string. Can anyone point me in the right direction?
UPDATE:
Working test below for anyone (and probably me!) in the future
describe('Example', () => {
test('searching requests the correct URL', async () => {
fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve([{a: 1}, {b: 2}]),
})
);
let searchButton: ReactWrapper;
const wrapper = mount(<Example/>);
const input = wrapper.find('input').at(0);
searchButton = wrapper.find('button').at(0);
input.simulate('change', {target: {value: 'Driver Name'}});
await act(async () => {
searchButton.simulate('click');
});
expect(searchButton.text()).toBe('Search');
expect(fetch.mock.calls.length).toBe(1);
expect(fetch.mock.calls[0][0]).toBe('/test?name=Driver Name');
});
});
I'm guessing that it is the clicking of the search button that is generating the act warning.
From react#16.9.0, act was changed to return a promise, meaning that you can avoid these types of warnings when testing async handlers.
Wrapping your search click simulation in an async act, might resolve the warning - but you might have to add a little timeout (not sure how this works with enzyme)
await act(() => {
searchButton.simulate('click');
})
Here are some more resources on the topic that might help you along the way:
secrets of the act(...) api
React’s sync and async act

Mock a custom service with jest and enzyme for a react component

I have a simple component that loads up some users from an api call, which I have abstracted in a service. Here is the Component:
export const Dashboard: FunctionComponent = () => {
const [users, setUsers] = useState<IUser[]>([]);
const [isLoading, setIsLoading] = useState(true);
const userService: UserService = UserService.get();
useEffect(() => {
userService.getUsers()
.then((data) => {
console.log(data)
setIsLoading(false)
setUsers(data)
})
.catch((error) => {
console.log("Could not load users: ", error);
setIsLoading(false)
setUsers([]);
});
}, []);
return (
isLoading
?
<div data-testid="loading">
<h4>Loading...</h4>
</div>
:
<div data-testid="users">
<UserList users={users}/>
</div>
);
}
export default Dashboard;
And my service looks like this:
export class UserService {
private static INSTANCE = new UserService();
private constructor() {}
public static get(): UserService {
return UserService.INSTANCE;
}
public async getUsers(): Promise<IUser[]> {
const response = await axios.get("api/users");
return response.data as IUsers[];
}
}
The reason I have extracted it in a .ts file is that I am planing to be reusing this service in another component and also add here other api calls.
So now I want to write a simple test for my Dashboard component, where I mock the UserService to return a promise and then test that my data-testid=users is rendered.
Here is my test:
configure({adapter: new Adapter()});
describe("User dashboard component", () => {
let userService: UserService;
const users = [
{
id: "0c8593e8-8fa6-4d40-b555-5ef812477c70",
name: "John",
age: 25
}
];
beforeAll(() => {
userService = UserService.get();
});
test("renders component", () => {
userService.getUsers = () => {
return Promise.resolve(users);
};
const dashboard = shallow(<Dashboard />);
expect(dashboard.find(<Dashboard />)).toBeTruthy();
expect(dashboard.find('[data-testid="users"]').length).toEqual(1);
expect(toJson(dashboard)).toMatchSnapshot();
});
test("loading", () => {
const dashboard = shallow(<Dashboard />);
expect(dashboard.find('[data-testid="loading"]').length).toEqual(1);
expect(toJson(dashboard)).toMatchSnapshot();
});
});
I don't want to mock the useState hook, but apparently my part where I resolve a Promise with users does nothing.
How do I achieve that? what is the best practice here? Thanks!
shallow doesn't support useEffect at this moment, mount should be used instead.
The component is rendered asynchronously and the test should be asynchronous too. Mocking methods by assignment is a bad practice because they cannot be restored, this results in test cross-contamination. A promise that makes it asynchronous should be exposed for chaining. In case of a spy it can be retrieved via Jest spy API.
It should be:
jest.spyOn(userService, 'getUsers').mockImplementation(() => Promise.resolve(users));
const dashboard = mount(<Dashboard />);
expect(userService.getUsers).toBeCalledTimes(1);
await act(async () => {
await userService.getUsers.mock.results[0].value;
});
...
Spies should be restored and cleared between tests in order for tests to not affect each other.

Enzyme onclick spy toHaveBeenCalled test does not work when testing on arrow function

how can i test the child component onclick.
Please see the below snippet.
// App.js
import React, {Component, Fragment} from 'react'
import Child from './child'
class App extends Component{
state = {
data: null,
enable: false
}
componentDidMount(){
this.getData()
}
getData = async () => {
const response = await fetch('http://www.example.com');
const data = await response.json();
this.setState({
data
})
}
_handleChildClick = () => {
this.setState({
enable: true
})
}
render(){
const {data, enable} = this.state
if(!data){
return (
<div>
Loading
</div>
)
}else{
<Fragment>
<Child
handleChildClick={this._handleChildClick}
/>
</Fragment>
}
}
}
export default App
import React from 'react';
const child = () => {
return(
<div>
<button
className="toggle"
onClick={props.handleChildClick}
>
Toggle
</button>
</div>
)
}
export default child
// App.test.js
import React from 'react';
import {enzyme} from 'enzyme';
import App from './App';
describe("App test cases", () => {
it('should trigger _handleChildClick', async () => {
window.fetch = jest.fn().mockImplementation(() => ({
status: 200,
json: () => new Promise((resolve, reject) => {
resolve(
{
name: "some data"
}
)
})
}))
const mountWrapper = await mount(<App />)
setTimeout(() => {
mountWrapper.update()
const SpyhandleChildClick = jest.spyOn(mountWrapper.instance(),'_handleChildClick')
mountWrapper.find('.toggle').simulate('click')
expect(SpyhandleChildClick).toHaveBeenCalled() // not called
},0)
})
})
Some important points to consider.
Asynchronous code in your tests
If you have to do asynchronous tasks in your tests you always have to await until the asynchronous stuff is completed.
setTimeout(() => {
mountWrapper.update()
const SpyhandleChildClick = jest.spyOn(mountWrapper.instance(),'_handleChildClick')
mountWrapper.find('.toggle').simulate('click')
expect(SpyhandleChildClick).toHaveBeenCalled() // not called
},0)
Above in your code you have a timeout segment. Any test condition inside this code block will not be evaluated since by the time they are evaluated you 'test session' will already be over due to the aync nature.
Testing arrow functions in React with enzyme - forceUpdate()
There seem to be a problem with the enzyme library where you have to force update the react component after spying for it to latch on to the method.
Please follow the github issue for more information : https://github.com/airbnb/enzyme/issues/365
I also cleaned up your test code a bit to make it more understandable!
// App.test.js
import React from 'react';
import {enzyme} from 'enzyme';
import App from './App';
describe("App test cases", () => {
it("should trigger _handleChildClick", async () => {
window.fetch = jest.fn().mockImplementation(() => ({
status: 200,
json: () =>
new Promise((resolve, reject) => {
resolve({
name: "some data"
});
})
}));
const mountWrapper = mount(<App />);
mountWrapper.update();
console.log("mountWrapper", mountWrapper.debug()); // showing the loader one
//[FIX]This code will block and wait for your asynchronous tasks to be completed
await new Promise(res => setTimeout(() => res(), 0));
mountWrapper.update();
console.log("mountWrapper", mountWrapper.debug()); // nothing showing
expect(mountWrapper.find(".toggle").length).toEqual(1);
//[FIX]Get a reference from the wrapper and force update after the spyOn call
const instance = mountWrapper.instance();
const spy = jest.spyOn(instance, "_handleChildClick");
instance.forceUpdate();
mountWrapper.find(".toggle").simulate("click");
expect(spy).toHaveBeenCalled();
});
});
Live Demo Link: Click on the 'Tests' tab on the browser to see the test results
https://codesandbox.io/s/mz21kpm37j

Resources