How to correctly test React with act - reactjs

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

Related

Test dispatch action using react testing-library

Hi I have a function as shown below:
MyComponent.js-------
const somefunction = () => {
if (someCondition) {
dispatch(someActionMethod({
id: 01,
empName: 'Adam'
}).then((result) => {
setSomeState(result.data);
})
)
}
}
Tried a lot of ways to mock someActionMethod with dispatch, but all efforts are failing.
My test file:
import React from 'react';
import * as redux from 'react-redux';
import { render } from '#testing-library/react';
const useDispatchSpy = jest.spyOn(reactRedux, 'useDispatch');
const mockDispatchFn = jest.fn();
describe('<MyComponent />', () => {
const props = {};
mockDispatchFn.mockReturnValue(
[{data1},{data2}]
);
let componentRendering;
beforeEach(() => {
componentRendering = render(
<MyComponent props={ props } />
)});
it('test component', ()=> {
expect(componentRendering.container.innerHTML).not.toBeNull();
});
});
Throws me error:
Cannot read properties of undefined (reading 'then')
My assumption from the error message is that your code is expecting a promise somewhere.
The stack trace will have info about the component throwing this error at a specific line.
I would recommend you to check your code and mock all the required methods
I hope this helps in resolving the error
Have you tried to mock useDispatch instead mocking someActionMethod?
import * as redux from 'react-redux';
const useDispatchSpy = jest.spyOn(redux, 'useDispatch');
const mockDispatchFn = jest.fn();
// One of these should work
mockDispatchFn.mockReturnValue([{data1}, {data2}]);
useDispatchSpy.mockReturnValue(mockDispatchFn);
With your updated response I'd try this:
describe('<MyComponent />', () => {
it('test component', () => {
const props = {};
const useDispatchSpy = jest.spyOn(reactRedux, 'useDispatch');
const mockDispatchFn = jest.fn();
mockDispatchFn.mockReturnValue(
[{ data1 }, { data2 }],
);
render(<MyComponent {...props} />);
// your assertions here
});
});
Are you using it with middleware? I am not sure that dispatch returns a promise nor any value without using middleware.
By the way, mocking a react redux hook is not a recommended way and you could actually see it is not even possible as of version 8 of react-redux package.

Mocking async function jest to test transitive state

I have a component that handles the login part of my app and I would like to test it's loading state.
Basically it does the following:
press a button to login
disable the button
make the request
enable back the button
What is the best way to mock AuthStore.login in this case to be able to test when isLoading is true and when it get back to false ?
I tried mocking with
const loginSpy = jest.spyOn(AuthStore, 'login').mockResolvedValueOnce({ success: true })
but then it returns immediately and I'm not able to test when the button should be disabled.
Here is a sample of the code
const Login = function(){
const [isLoading, setIsLoading] = useState(false)
async function onPressGoogleLogin() {
setIsLoading(true)
const {
success,
error
} = await AuthStore.login()
setIsLoading(false)
}
return ...
}
My test using #testing-library/react-native looks like this.
it.only('testing login', async () => {
const { getByA11yHint } = render(<LoginScreen componentId="id-1" />)
const btn = getByA11yHint('login')
expect(btn).not.toBeDisabled()
await act(async () => {
await fireEvent.press(btn)
})
expect(btn).toBeDisabled()
expect(within(btn).getByTestId('loader')).toBeTruthy()
})
Any ideas ?
Test 1
Mock AuthStore.login() to not return a resolving promise. Mock it to return a pending promise. Inquire about the state of things when the button is pressed.
Test 2
Then, in a separate test, mock AuthStore.login() to return a resolving promise. Inquire about the state of things when the button is pressed.
Test 3
Bonus points: in a separate test, mock AuthStore.login() to return a rejecting promise. Inquire about the state of things when the button is pressed.
So I managed to get it to work in one test (with Jest 27)
import { act, fireEvent, render } from '#testing-library/react-native'
import React, { useCallback, useState } from 'react'
import { Pressable } from 'react-native'
const Utils = {
sleep: (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms))
}
}
const Component = function () {
const [refreshing, setRefresh] = useState(false)
const refresh = useCallback(async () => {
setRefresh(true)
await Utils.sleep(5000)
setRefresh(false)
}, [])
return <Pressable testID="btn" onPress={refresh} disabled={refreshing} />
}
const sleepSpy = jest.spyOn(Utils, 'sleep').mockImplementation(() => {
return new Promise((resolve) => {
process.nextTick(resolve)
})
})
describe('Test', () => {
it('test', async () => {
const { getByTestId } = render(<Component />)
const btn = getByTestId('btn')
expect(btn.props.accessibilityState.disabled).toBeFalsy()
let f = fireEvent.press(btn)
expect(sleepSpy).toBeCalledTimes(1)
expect(btn.props.accessibilityState.disabled).toBeTruthy()
await act(async () => {
await f
})
expect(btn.props.accessibilityState.disabled).toBeFalsy()
})
})

error when creating multiple asynchronous tests with act function jest js

in this case I am doing an example project which tries to show a phrase and the author of the phrase the first time the component is loaded and when a button is clicked to obtain a new phrase. The problem with testing is I am trying to simulate the way the user interacts with the application and having two tests asynchronously where each one has the act () function I get the following error:
console.error node_modules/react-dom/cjs/react-dom-test-utils.development.js:87
Warning: You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one.
console.error node_modules/react-dom/cjs/react-dom.development.js:88
Warning: An update to QuoteContainer 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.
in QuoteContainer (created by WrapperComponent)
in WrapperComponent
console.error node_modules/react-dom/cjs/react-dom.development.js:88
Warning: An update to QuoteContainer 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.
in QuoteContainer (created by WrapperComponent)
in WrapperComponent
console.error node_modules/react-dom/cjs/react-dom.development.js:88
Warning: An update to QuoteContainer 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.
in QuoteContainer (created by WrapperComponent)
in WrapperComponent
The test is passing but it generates this alert. I already looked for alternatives such as avoiding asynchronous testing but when doing it the test does not pass, it forces me to put the await in the act.
This example is simple but I would like to apply this same test in more complex applications to verify the correct operation of it. So it is probably necessary to have more asynchronous tests.
Here is the container component:
import React, {
useEffect,
useState,
} from "react";
import Quote from "./quote.component";
import { getQuote } from "../../repository/quote.repository";
const QuoteContainer = () => {
const [quote, setQuote] = useState("");
const [author, setAuthor] = useState("");
const [isLoading, setLoading] = useState(true);
useEffect(() => {
getQuoteData();
}, []);
const newQuoteHandler = () => {
getQuoteData();
};
const getQuoteData = async () => {
const {
quoteText,
quoteAuthor,
} = await getQuote();
setAuthor(quoteAuthor);
setQuote(quoteText);
setLoading(false);
};
return (
<Quote
isLoading={isLoading}
quote={quote}
author={author}
newQuoteHandler={newQuoteHandler}
/>
);
};
export default QuoteContainer;
And the test related to the container:
import React from "react";
import { server, rest } from "../../mocks/server";
import waitForExpect from "wait-for-expect";
import { mount } from "../../enzymeConfig";
import QuoteContainer from "./quote.container";
import { act } from "react-dom/test-utils";
import { QuoteText } from "../quote/quote.style";
import { SpinnerContainer } from "../withSpinner/withSpinner.style";
describe("testing quote api", () => {
it("should render spinner on start", () => {
const wrapper = mount(<QuoteContainer />);
expect(
wrapper.find(SpinnerContainer)
).toHaveLength(1);
});
it("should render actual component on load information", async (done) => {
expect.assertions(1);
const wrapper = mount(<QuoteContainer />);
await act(async () => {
await waitForExpect(() => {
wrapper.update();
expect(
wrapper.find(QuoteText).text()
).toEqual(
"Know how to listen, and you will profit even from those who talk badly."
);
done();
});
});
});
it("should change quote and author when Quote Button click", async (done) => {
const wrapper = mount(<QuoteContainer />);
await act(async () => {
await waitForExpect(() => {
wrapper.update();
expect(
wrapper.find(QuoteText).text()
).toEqual(
"Know how to listen, and you will profit even from those who talk badly."
);
done();
});
});
rest.get(
"apiURL",
(req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
quoteText: "this is an other frase",
quoteAuthor: "Plutarch ",
senderName: "",
senderLink: "",
})
)
);
wrapper
.find(CustomButton)
.at(0)
.simulate("click");
await act(async () => {
await waitForExpect(() => {
wrapper.update();
expect(
wrapper.find(QuoteText).text()
).toEqual("this is an other frase");
done();
});
});
});
});
Thanks from now

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

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.

Test intermediary state in async handler with React and Enzyme

Despite reading the documentation of enzyme, and act, I could not find a response to my use case because the examples only show simple use cases.
I have a React component displaying a button. The onClick handler sets a loading boolean and calls an external API. I want to assert that the component shows the loading indicator when we click on the button.
Here is the component:
export default function MyButton(): ReactElement {
const [loading, setLoading] = useState<boolean>(false);
const [data, setData] = useState<any>(null);
const onClick = async (): Promise<void> => {
setLoading(true);
const response = await fetch('/uri');
setData(await response.json());
setLoading(false);
};
if (loading) {
return <small>Loading...</small>;
}
return (
<div>
<button onClick={onClick}>Click Me!</button>
<div>
{data}
</div>
</div>
);
}
And here is the test:
test('should display Loading...', async () => {
window.fetch = () => Promise.resolve({
json: () => ({
item1: 'item1',
item2: 'item2',
}),
});
const component = mount(<MyButton />);
// Case 1 ✅ => validates the assertion BUT displays the following warning
component.find('button').simulate('click');
// Warning: An update to MyButton 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 [URL to fb removed because SO does not accept it]
// Case 2 ❌ => fails the assertion AND displays the warning above
act(() => {
component.find('button').simulate('click');
});
// Case 3 ❌ => fails the assertion BUT does not display the warning
await act(async () => {
component.find('button').simulate('click');
});
expect(component.debug()).toContain('Loading...');
});
As you can see, if I get rid of the warning, my test is not satisfying anymore as it waits for the promise to resolve. How can we assert the intermediate state change while using act?
Thanks.
Just resolve promise manually:
const mockedData = {
json: () => ({
item1: 'item1',
item2: 'item2',
}),
};
let resolver;
window.fetch = () => new Promise((_resolver) => {
resolver = _resolver;
});
// ....
await act(async () => {
component.find('button').simulate('click');
});
expect(component.debug()).toContain('Loading...');
resolver(mockedData);
expect(component.debug()).not.toContain('Loading...');
PS but in sake of readability I'd rather have 2 separate tests: one with new Promise(); that never resolves and another with Promise.resolve(mockedData) that would be resolved automatically

Resources