My functional component uses the useEffect hook to fetch data from an API on mount. I want to be able to test that the fetched data is displayed correctly.
While this works fine in the browser, the tests are failing because the hook is asynchronous the component doesn't update in time.
Live code:
https://codesandbox.io/s/peaceful-knuth-q24ih?fontsize=14
App.js
import React from "react";
function getData() {
return new Promise(resolve => {
setTimeout(() => resolve(4), 400);
});
}
function App() {
const [members, setMembers] = React.useState(0);
React.useEffect(() => {
async function fetch() {
const response = await getData();
setMembers(response);
}
fetch();
}, []);
return <div>{members} members</div>;
}
export default App;
App.test.js
import App from "./App";
import React from "react";
import { mount } from "enzyme";
describe("app", () => {
it("should render", () => {
const wrapper = mount(<App />);
console.log(wrapper.debug());
});
});
Besides that, Jest throws a warning saying:
Warning: An update to App inside a test was not wrapped in act(...).
I guess this is related? How could this be fixed?
Ok, so I think I've figured something out. I'm using the latest dependencies right now (enzyme 3.10.0, enzyme-adapter-react-16 1.15.1), and I've found something kind of amazing. Enzyme's mount() function appears to return a promise. I haven't seen anything about it in the documentation, but waiting for that promise to resolve appears to solve this problem. Using act from react-dom/test-utils is also essential, as it has all the new React magic to make the behavior work.
it('handles async useEffect', async () => {
const component = mount(<MyComponent />);
await act(async () => {
await Promise.resolve(component);
await new Promise(resolve => setImmediate(resolve));
component.update();
});
console.log(component.debug());
});
Following on from #user2223059's answer it also looks like you can do:
// eslint-disable-next-line require-await
component = await mount(<MyComponent />);
component.update();
Unfortunately you need the eslint-disable-next-line because otherwise it warns about an unnecessary await... yet removing the await results in incorrect behaviour.
I was having this problem and came across this thread. I'm unit testing a hook but the principles should be the same if your async useEffect code is in a component. Because I'm testing a hook, I'm calling renderHook from react hooks testing library. If you're testing a regular component, you'd call render from react-dom, as per the docs.
The problem
Say you have a react hook or component that does some async work on mount and you want to test it. It might look a bit like this:
const useMyExampleHook = id => {
const [someState, setSomeState] = useState({});
useEffect(() => {
const asyncOperation = async () => {
const result = await axios({
url: `https://myapi.com/${id}`,
method: "GET"
});
setSomeState(() => result.data);
}
asyncOperation();
}, [id])
return { someState }
}
Until now, I've been unit testing these hooks like this:
it("should call an api", async () => {
const data = {wibble: "wobble"};
axios.mockImplementationOnce(() => Promise.resolve({ data}));
const { result } = renderHook(() => useMyExampleHook());
await new Promise(setImmediate);
expect(result.current.someState).toMatchObject(data);
});
and using await new Promise(setImmediate); to "flush" the promises. This works OK for simple tests like my one above but seems to cause some sort of race condition in the test renderer when we start doing multiple updates to the hook/component in one test.
The answer
The answer is to use act() properly. The docs say
When writing [unit tests]... react-dom/test-utils provides a helper called act() that makes sure all updates related to these “units” have been processed and applied to the DOM before you make any assertions.
So our simple test code actually wants to look like this:
it("should call an api on render and store the result", async () => {
const data = { wibble: "wobble" };
axios.mockImplementationOnce(() => Promise.resolve({ data }));
let renderResult = {};
await act(async () => {
renderResult = renderHook(() => useMyExampleHook());
})
expect(renderResult.result.current.someState).toMatchObject(data);
});
The crucial difference is that async act around the initial render of the hook. That makes sure that the useEffect hook has done its business before we start trying to inspect the state. If we need to update the hook, that action gets wrapped in its own act block too.
A more complex test case might look like this:
it('should do a new call when id changes', async () => {
const data1 = { wibble: "wobble" };
const data2 = { wibble: "warble" };
axios.mockImplementationOnce(() => Promise.resolve({ data: data1 }))
.mockImplementationOnce(() => Promise.resolve({ data: data2 }));
let renderResult = {};
await act(async () => {
renderResult = renderHook((id) => useMyExampleHook(id), {
initialProps: { id: "id1" }
});
})
expect(renderResult.result.current.someState).toMatchObject(data1);
await act(async () => {
renderResult.rerender({ id: "id2" })
})
expect(renderResult.result.current.someState).toMatchObject(data2);
})
I was also facing similar issue. To solve this I have used waitFor function of React testing library in enzyme test.
import { waitFor } from '#testing-library/react';
it('render component', async () => {
const wrapper = mount(<Component {...props} />);
await waitFor(() => {
wrapper.update();
expect(wrapper.find('.some-class')).toHaveLength(1);
}
});
This solution will wait for our expect condition to fulfill. Inside expect condition you can assert on any HTML element which get rendered after the api call success.
this is a life saver
export const waitForComponentToPaint = async (wrapper: any) => {
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0));
await wait(0);
wrapper.update();
});
};
in test
await waitForComponentToPaint(wrapper);
After lots of experimentation I have come up with a solution that finally works for me, and hopefully will fit your purpose.
See below
import React from "react";
import { mount } from "enzyme";
import { act } from 'react-dom/test-utils';
import App from "./App";
describe("app", () => {
it("should render", async () => {
const wrapper = mount(<App />);
await new Promise((resolve) => setImmediate(resolve));
await act(
() =>
new Promise((resolve) => {
resolve();
})
);
wrapper.update();
// data loaded
});
});
Related
I'm in the middle of trying to get as close to 100% unit test coverage with my React application as possible, however I'm in a bit of a pickle with trying to get test coverage on the useAsync hook. I took the following code from use react hooks:
import { useState, useEffect, useCallback } from 'react';
const useAsync = (asyncFunction, immediate = true) => {
const [status, setStatus] = useState('idle');
const [value, setValue] = useState(null);
const [error, setError] = useState(null);
// The execute function wraps asyncFunction and
// handles setting state for pending, value, and error.
// useCallback ensures the below useEffect is not called
// on every render, but only if asyncFunction changes.
const execute = useCallback(() => {
setStatus('pending');
setValue(null);
setError(null);
return asyncFunction()
.then(response => {
setValue(response);
setStatus('success');
})
.catch(error => {
setError(error);
setStatus('error');
});
}, [asyncFunction]);
// Call execute if we want to fire it right away.
// Otherwise execute can be called later, such as
// in an onClick handler.
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { execute, status, value, error };
};
And it works fine. I've not done anything funky or included any additional testing libraries/dependencies on top of what you'd get when you run npx create-react-app ....
Because I couldn't find a suitable solution for my team to test react hooks without relying on additional dependencies, i.e. react-hooks-testing-library, I created a mock component to wrap the hook in, then I just test the different possible states like so:
import React from "react";
import { render, cleanup } from "#testing-library/react";
import { useAsync } from "../../hooks";
afterEach(cleanup);
const MockComponent = (props) => {
const { status } = useAsync(props.callback, props.immediate);
return <p id="test">{status}</p>
};
const defaultCallback = () => {
new Promise((resolve) => {
setTimeout(() => {
resolve("Success!");
}, 1000)
});
};
// Example of a test that runs fine, no problem at all!
test("useAsync gets to a pending state", () => {
const { container } = render(<MockComponent callback={defaultCallback} immediate={true} />
const pTag = container.querySelector("p#test");
expect(pTag.textContent).toBe("idle");
});
// This is where I get my issue...
test("useAsync gets to a success state", (done) => {
const { container } = render(<MockComponent callback={defaultCallback} immediate={true} />
const pTag = container.querySelector("p#test");
setTimeout(() => {
expect(pTag.textContent).toBe("success");
done();
}, 2000);
});
// Some other tests...
When I run these two tests that I've included above, they actually run fine and if I look at the code coverage report(s) that are generated, it's 100% coverage. In the console, though, when running these tests, when I run the second one, I get the following error and I'm not sure how to resolve it. I have tried to wrap the entire body of the test in act, but that made no difference, so I'm not entirely sure what the best approach may be here.
Here's the console output:
console.error
Warning: An update to MockComponent inside a tests 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 MockComponent....
As I've said, I've tried to wrap my entire test(s) with an act, but I've had no luck there. What I have found interesting is that in the console output, when it apparently errors, it also highlights this snippet of the hook:
.then(response => {
setValue(response);
// ^
setStatus('success');
// ^
})
.catch(error => {
setError(error);
Should I perhaps ignore this issue? After all when all of my tests are run, no tests/test suite fails or anything.
Okay, so after much tinkering & googling, I found an answer to my problem, thankfully it's relatively straightforward! 😃 - The short answer being the use of waitFor.
I made a couple of other modifications, i.e. assigned the timeout delay to a variable called timeout, but nothing major.
import React from "react";
import { render, cleanup, waitFor } from "#testing-library/react";
import { useAsync } from "../../hooks";
afterEach(cleanup);
let timeout = 1000;
beforeEach(() => {
timeout = 1000
});
const MockComponent = (props) => {
const { status } = useAsync(props.callback, props.immediate);
return <p id="test">{status}</p>
};
const defaultCallback = () => {
new Promise((resolve) => {
setTimeout(() => {
resolve("Success!");
}, timeout)
});
};
// Example of a test that runs fine, no problem at all!
test("useAsync gets to a pending state", () => {
const { container } = render(<MockComponent callback={defaultCallback} immediate={true} />
const pTag = container.querySelector("p#test");
expect(pTag.textContent).toBe("idle");
});
// This now works! :D
test("useAsync gets to a success state", async () => {
timeout = 0;
const { container } = render(<MockComponent callback={defaultCallback} immediate={true} />
const pTag = container.querySelector("p#test");
expect(pTag).toBeInTheDocument();
// Yes!
await waitFor(() => {
expect(pTag.textContent).toBe("success");
});
});
// Some other tests...
I'm following a tutorial on React testing. The tutorial has a simple component like this, to show how to test asynchronous actions:
import React from 'react'
const TestAsync = () => {
const [counter, setCounter] = React.useState(0)
const delayCount = () => (
setTimeout(() => {
setCounter(counter + 1)
}, 500)
)
return (
<>
<h1 data-testid="counter">{ counter }</h1>
<button data-testid="button-up" onClick={delayCount}> Up</button>
<button data-testid="button-down" onClick={() => setCounter(counter - 1)}>Down</button>
</>
)
}
export default TestAsync
And the test file is like this:
import React from 'react';
import { render, cleanup, fireEvent, waitForElement } from '#testing-library/react';
import TestAsync from './TestAsync'
afterEach(cleanup);
it('increments counter after 0.5s', async () => {
const { getByTestId, getByText } = render(<TestAsync />);
fireEvent.click(getByTestId('button-up'))
const counter = await waitForElement(() => getByText('1'))
expect(counter).toHaveTextContent('1')
});
The terminal says waitForElement has been deprecated and to use waitFor instead.
How can I use waitFor in this test file?
If you're waiting for appearance, you can use it like this:
it('increments counter after 0.5s', async() => {
const { getByTestId, getByText } = render(<TestAsync />);
fireEvent.click(getByTestId('button-up'));
await waitFor(() => {
expect(getByText('1')).toBeInTheDocument();
});
});
Checking .toHaveTextContent('1') is a bit "weird" when you use getByText('1') to grab that element, so I replaced it with .toBeInTheDocument().
Would it be also possible to wrap the assertion using the act
function? Based on the docs I don't understand in which case to use
act and in which case to use waitFor.
The answer is yes. You could write this instead using act():
import { act } from "react-dom/test-utils";
it('increments counter after 0.5s', async() => {
const { getByTestId, getByText } = render(<TestAsync />);
// you wanna use act() when there is a render to happen in
// the DOM and some change will take place:
act(() => {
fireEvent.click(getByTestId('button-up'));
});
expect(getByText('1')).toBeInTheDocument();
});
Hope this helps.
Current best practice would be to use findByText in that case. This function is a wrapper around act, and will query for the specified element until some timeout is reached.
In your case, you can use it like this:
it('increments counter after 0.5s', async () => {
const { findByTestId, findByText } = render(<TestAsync />);
fireEvent.click(await findByTestId('button-up'))
const counter = await findByText('1')
});
You don't need to call expect on its value, if the element doesn't exist it will throw an exception
You can find more differences about the types of queries here
I am trying to test custom hook. I want to know is setState function fire or not.
here is my custom hook
import React from "react";
import axios from "axios";
export default () => {
const [state, setState] = React.useState([]);
const fetchData = async () => {
const res = await axios.get("https://5os4e.csb.app/data.json");
setState(res.data);
};
React.useEffect(() => {
(async () => {
await fetchData();
})();
}, []);
return { state };
};
now I am trying to test this custom hook. I want to know is setState function fire or not .
I tried like this
import moxios from "moxios";
import React from "react";
import { act, renderHook, cleanup } from "#testing-library/react-hooks";
import useTabData from "./useTabData";
describe("use tab data", () => {
beforeEach(() => {
moxios.install();
});
afterEach(() => {
moxios.uninstall();
});
describe("non-error response", () => {
// create mocks for callback arg
const data = [
{
name: "hello"
}
];
let mockSetCurrentGuess = jest.fn();
beforeEach(async () => {
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: data
});
});
});
test("calls setState with data", async () => {
React.useState = jest.fn(() => ["", mockSetCurrentGuess]);
const { result, waitForNextUpdate } = renderHook(() => useTabData());
console.log(result);
//expect(mockSetCurrentGuess).toHaveBeenCalledWith(data);
});
});
});
You should not mock the React internals. This is incorrect. Either ways, this code has no effect in mocking due to closures. Even if it worked, no point in testing if you are mocking the real implementation, isn't it ? :)
I would recommend to try to get grasp of what react hook is doing in your code.
You have a state in your custom hook:
const [state, setState] = React.useState([]);
.
.
return [state]; //you are returning the state as ARRAY
#testing-library/react-hooks allows you to debug and get value of current outcome of hook.
const { result, waitForNextUpdate } = renderHook(() => useTabData());
const [foo] = result.current; // the array you returned in hook
expect(foo).toEqual('bar'); //example assertion
I would stop here and allow you to learn and debug.
_onRefresh = async () => {
this.setState({isRefreshing: true});
await ExpenseReportHistoryBusiness.GetInstance().getListExpenseReportRequest();
this.setState({isRefreshing: false});
this.forceUpdate();
this.focusListener = this.props.navigation.addListener(
'didFocus',
async () => {
this.setState({displayMessage: true});
await ExpenseReportHistoryBusiness.GetInstance().getListExpenseReportRequest();
this.forceUpdate();
},
);
};
componentWillUnmount() {
this.focusListener.remove();
}
i got this code that i need to convert into react hooks code, i know pretty much everything related to useEffect() except for this focuslistener and componentwillunmount thing, and i got the same code in componentdidmount, but i think i know how to do that one
You should replace the code with something like below,
Here the loadItems function loads items from your service or backend this function will be called from the focus effect and also the refresh.
function Scree1({ navigation }) {
const [displayMessage, setDisplayMessage] = React.useState(false);
const loadItems = async () => {
setDisplayMessage(true);
await ExpenseReportHistoryBusiness.GetInstance().getListExpenseReportRequest();
setDisplayMessage(false);
};
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', async () => {
await loadItems();
});
// will replace the component will unmount
return unsubscribe;
}, [navigation]);
return <View />;
}
This code is not tested as it requires your functions but will give you the idea.
I'm trying to transition a small example from using shouldComponentUpdate to using React hooks. I also want to write some unit tests within Jest, which is where I'm struggling.
Note: that I've seen some existing questions/answers and I do not want to use a setTimeout for my testing.
The code I have at the moment looks like this, firstly the component:
export default function HelloStarWars(props) {
const data = useFetch(`https://swapi.co/api/people/${props.id}`);
const { name } = data;
return (
<h1>Hello {name || "..."}!</h1>
);
}
where useFetch is defined as follows:
import { useState, useEffect } from "react";
export default function useFetch(url) {
const [data, setData] = useState({});
async function getData() {
const response = await fetch(url);
const data = await response.json();
setData(data);
}
useEffect(() => {
getData();
}, []);
return data;
}
Now the actual bit I'm struggling with, is I can't work out how to convert this code that I put together. Essentially I have no componentDidMount() function to await anymore - so what do I wait upon to ensure that the data has been retrieved?
describe("ComponentDidMount", () => {
beforeEach(() => {
fetch.resetMocks();
});
it("Obtains result and sets data correctly", async () => {
const wrapper = await mount(<HelloStarWars id="1" />);
const instance = wrapper.instance();
fetch.mockResponseOnce(JSON.stringify({ name: 'Jedi Master' }));
await instance.componentDidMount();
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
});
One option I'm aware of is I could, spy on the useFetch and mock the return result. I will do that if I have to, but at the moment I like the jest-fetch-mock library that I'm using and the capabilities it offers.