Jest|Enzyme|React testing with Async ComponentDidMount()
Writing tests is not my favorite part of the day. Then why write a post about tests? Well, for two reasons: 1) I learned something that was a total PITA to figure out, and 2) I am hoping this article is found for others who encounter the same issue and it helps ease the pain with some code examples.
🤢 ==> 😀
TL;DR: This post is not about why you need async/await or Promise/Resolve functionality. There are plenty of articles on that. flushPromises()
solves the bug or feature request depending how you look at it.
Background: I was building a feature out requiring asynchronous fetch() to our endpoint, as soon as our React component mounts. When the endpoint returns, we handle it, and update state. Functionally, the component wasn’t too bad to build out. And yes, there are 2 fetches in this case:
async componentDidMount() {
// this feature will have to make 2 fetches as the 2nd is dependent on the first. Terrible architecture, but due to backend limits at time of code.
// getAllUsers() gets the current list of agents. We need to eliminate duplicates and remove current user-agent from the list of agents
// then we take this data and pass it to 2nd endpoint which will return live chat data for UI to be updated. const allUsers = await API.getAllUsers(); if (allUsers.length) {
const sortedAgents = filterSortListOfSingleUsers(currentUser, allUsers, allowedWidgets); this.setState({ sortedAgents, loading: false }); const liveAgentDetails = await API.fetchAgentsLiveInfo(sortedAgents,this.props.loggedInUser); if (!isEmpty(liveAgentDetails)) {
this.setState({ liveAgentDetails });
}
} else {
this.setState({ loading: false, infoText: "Please try again."});
}
}render() {
// DumbComponentWrapper receives updated state as prop after promise resolves return (
<SmartComponentWrapper>
<DumbComponentWrapper liveAgentDetails={this.state.liveAgentDetails}>
</SmartComponentWrapper>
)
}
NOTE: You may be asking, why not dispatch a Redux action for this fetch, and dispatch a reducer action to handle the response? In essence — this component is a single-use-&-throw-away results UI. It will always fetch
to get most current data. By binding our data to the component, it’s encapsulated and won’t require the store to be updated which has some performance advantages. Sometimes it’s easy to use Redux because it’s there, even if it’s not required.
On to the tests!
First we need to mock the response of the API’s with Jest’s mockImplementation for when our component is mounted by Enzyme’s shallow
render:
yourAPI.mockImplementation(() => {
return Promise.resolve(mockFixtureData);
});
And now our simple test:
it("fetches agents live info data and pass to dumb component", () => {
expect(dumbComponentWrapper.prop("liveAgentDetails")).toEqual(
agentsInfoFixture);});
If you run this now, you may see “state” is updated for the SmartComponent, but the props passed to DumbComponent are empty:
liveAgentDetails={Object {}}
But there should be data there!?!?! Well, it’s because the promises have not resolved. You can even verify the loading state:
isLoadingUsers={true}
So there was one more workaround found, but it’s not scalable. We have 2 promises (another issue as I said, but it proves the point):
await smartComponentWrapper.instance().busy;
smartComponentWrapper = smartComponentWrapper.update();
So if I add two of these, my component is manually updated and props get updated with some magic dust 🧙♂️I don’t feel comfortable still. More digging to do.
If you search around for async componentDidMount ezyme jest promise resolve
you will see a bunch of options, probably this is the most popular: enzyme/issues/1587:
Essentially you will return a Promise.resolve()
with .then()
chains, and the tests live in the last chain. These also includes update()
but also, it’s not scalable. At this point I felt a bit stuck. Maybe it was because I hadn’t seen one of the GitHub issue links inside this thread. Maybe it’s because I was frustrated with this classic example of TESTS TAKE LONGER THAN THE CODE.
A simple helper function solved the issue:flushPromises()
to the rescue!
function flushPromises() {
return new Promise(resolve => setImmediate(resolve));
}
So now after you call your shallow()
render, call flushPromises()
What this does is all pending promises are resolved without requiring done()
callback, which means the React lifecycle method performs as expected, and therefore the DumbComponent gets props it needs.
Keep in mind order of your assignment to DumbComponent variable is important
So here’s the whole test setup:
describe("<AgentsStatusModal />", () => {
beforeEach(async () => {
// Set async mocks that await resolve and return data await API.fetchAgentsLiveInfo.mockImplementation(() => {
return Promise.resolve(agentsInfoFixture);
});
await API.getAllUsers.mockImplementation(() => {
return Promise.resolve(allUsersFixture);
});
// initiailze lifecycle & trigger async functions;
wrapper = shallow(<AgentsStatusModal {...props} />); // flush promises so that state is updated;
await flushPromises();//after promises have been resolved, we render the inner components
wrapper = wrapper.find(Modal).renderProp("render")(); dumbComponentWrapper = smartComponentWrapper.find(AgentsStatusModalContent); }); it("props are passed to dumb component", () => {
expect(dumbComponentWrapper.prop("liveAgentDetails")).toEqual(
agentsInfoFixture);
});});
Good luck and thanks for reading!