Trigger Useeffect In Jest Testing
Solution 1:
Here's a minimal, complete example of mocking fetch
. Your component pretty much boils down to the generic fire-fetch-and-set-state-with-response-data idiom:
importReact, {useEffect, useState} from"react";
exportdefaultfunctionUsers() {
const [users, setUsers] = useState([]);
useEffect(() => {
(async () => {
const res = awaitfetch("https://jsonplaceholder.typicode.com/users");
setUsers(await res.json());
})();
}, []);
return<p>there are {users.length} users</p>;
};
Feel free to run this component in the browser:
<scripttype="text/babel"defer>const {useState, useEffect} = React;
constUsers = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
(async () => {
const res = awaitfetch("https://jsonplaceholder.typicode.com/users");
setUsers(await res.json());
})();
}, []);
return<p>there are {users.length} users</p>;
};
ReactDOM.render(<Users />, document.body);
</script><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
You can see the component initially renders a value of 0, then when the request arrives, all 10 user objects are in state and a second render is triggered showing the updated text.
Let's write a naive (but incorrect) unit test, mocking fetch
since it doesn't exist in Node:
import {act} from"react-dom/test-utils";
importReactfrom"react";
importEnzyme, {mount} from"enzyme";
importAdapterfrom"enzyme-adapter-react-16";
importUsersfrom"../src/Users";
Enzyme.configure({adapter: newAdapter()});
describe("Users", () => {
let wrapper;
let users;
beforeEach(() => {
const mockResponseData = [{id: 1}, {id: 2}, {id: 3}];
users = mockResponseData.map(e => ({...e}));
jest.clearAllMocks();
global.fetch = jest.fn(async () => ({
json: async () => mockResponseData
}));
wrapper = mount(<Users />);
});
it("renders a count of users", () => {
const p = wrapper.find("p");
expect(p.exists()).toBe(true);
expect(p.text()).toEqual("there are 3 users");
});
});
All seems well--we load up the wrapper, find the paragraph and check the text. But running it gives:
Error: expect(received).toEqual(expected) // deep equality
Expected: "there are 3 users"
Received: "there are 0 users"
Clearly, the promise isn't being awaited and the wrapper is not registering the change. The assertions run synchronously on the call stack as the promise waits in the task queue. By the time the promise resolves with the data, the suite has ended.
We want to get the test block to await
the next tick, that is, wait for the call stack and pending promises to resolve before running. Node provides setImmediate
or process.nextTick
for achieving this.
Finally, the wrapper.update()
function enables synchronization with the React component tree so we can see the updated DOM.
Here's the final working test:
import {act} from"react-dom/test-utils";
importReactfrom"react";
importEnzyme, {mount} from"enzyme";
importAdapterfrom"enzyme-adapter-react-16";
importUsersfrom"../src/Users";
Enzyme.configure({adapter: newAdapter()});
describe("Users", () => {
let wrapper;
let users;
beforeEach(() => {
const mockResponseData = [{id: 1}, {id: 2}, {id: 3}];
users = mockResponseData.map(e => ({...e}));
jest.clearAllMocks();
global.fetch = jest.fn(async () => ({
json: async () => mockResponseData
}));
wrapper = mount(<Users />);
});
it("renders a count of users", async () => {
// ^^^^^awaitact(() =>newPromise(setImmediate)); // <--
wrapper.update(); // <--const p = wrapper.find("p");
expect(p.exists()).toBe(true);
expect(p.text()).toEqual("there are 3 users");
});
});
The new Promise(setImmediate)
technique also helps us assert on state before the promise resolves. act
(from react-dom/test-utils
) is necessary to avoid Warning: An update to Users inside a test was not wrapped in act(...)
that pops up with useEffect
.
Adding this test to the above code also passes:
it("renders a count of 0 users initially", () => {
returnact(() => {
const p = wrapper.find("p");
expect(p.exists()).toBe(true);
expect(p.text()).toEqual("there are 0 users");
returnnewPromise(setImmediate);
});
});
The test callback is asynchronous when using setImmediate
, so returning a promise is necessary to ensure Jest waits for it correctly.
This post uses Node 12, Jest 26.1.0, Enzyme 3.11.0 and React 16.13.1.
Solution 2:
With jest you can always mock. So what you need is:
- In your unit test mock useEffect from React
jest.mock('React', () => ({
...jest.requireActual('React'),
useEffect: jest.fn(),
}));
That allows to mock only useEffect and keep other implementation actual.
- Import useEffect to use it in the test
import { useEffect } from'react';
- And finally in your test call the mock after the component is rendered
useEffect.mock.calls[0](); // <<-- That will call implementation of your useEffect
Solution 3:
useEffect has already been triggered and working, the point is that its an async
operation. So you need to wait for the fetch to be completed. one of the ways that you can do that is:
1. write your assertion(s)
2. specify the number of assertion(s) in your test, so that jest knows that it has to wait for the operation to be completed.
it('handles groupId and api call ', () => {
// the effect will get called// the effect will call getGroups// the iframe will contain group parameters for the given groupId
expect.assertions(1)
const wrapper = shallow(<UsageWidgetsurface={`${USAGE_SURFACES.metrics}`} groupId={2} />)
wrapper.update()
expect(whatever your expectation is)
});
since in this example i just wrote on assertion,
expect.assertions(1)
if you write more, you need to change the number.
Solution 4:
You can set a timeout to asynchronously check if the the expected condition has been met.
it('handles groupId and api call ', (done) => {
const wrapper = shallow(<UsageWidgetsurface={`${USAGE_SURFACES.metrics}`} groupId={1} />)
setTimeout(() => {
expect(wrapper.find("iframe").prop('src')).toBeTruthy(); // or whateverdone();
}, 5000);
}
The timeout lets you wait for the async fetch to complete. Call done()
at the end to signal that the it()
block is complete.
You probably also want to do a mock implementation of your getGroups
function so that you're not actually hitting a network API every time you test your code.
Post a Comment for "Trigger Useeffect In Jest Testing"