Reputation: 1982
I have been trying to understand how to test a mounted component that runs async fetch during componentDidMount
.
The issue is that I can make it wait for the initial fetch to trigger, but not wait to resolve all of the chain from the promise.
Here is an example:
import React from "react";
class App extends React.Component {
state = {
groceries: [],
errorStatus: ""
};
componentDidMount() {
console.log("calling fetch");
fetch("/api/v1/groceries")
.then(this.checkStatus)
.then(this.parseJSON)
.then(this.setStateFromData)
.catch(this.setError);
}
checkStatus = results => {
if (results.status >= 400) {
console.log("bad status");
throw new Error("Bad Status");
}
return results;
};
setError = () => {
console.log("error thrown");
return this.setState({ errorStatus: "Error fetching groceries" });
};
parseJSON = results => {
console.log("parse json");
return results.json();
};
setStateFromData = data => {
console.log("setting state");
return this.setState({ groceries: data.groceries });
};
render() {
const { groceries } = this.state;
return (
<div id="app">
{groceries.map(grocery => {
return <div key={grocery.id}>{grocery.item}</div>;
})}
</div>
);
}
}
export default App;
Test:
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import React from 'react';
import { mount } from 'enzyme'
import App from './App';
Enzyme.configure({ adapter: new Adapter() });
const mockResponse = (status, statusText, response) => {
return new window.Response(response, {
status: status,
statusText: statusText,
headers: {
'Content-type': 'application/json'
}
});
};
describe('App', () => {
describe('componentDidMount', () => {
it('sets the state componentDidMount', async () => {
console.log('starting test for 200')
global.fetch = jest.fn().mockImplementation(() => Promise.resolve(
mockResponse(
200,
null,
JSON.stringify({
groceries: [
{ item: 'nuts', id: 10 }, { item: 'greens', id: 3 }
]
})
)
));
const renderedComponent = await mount(<App />)
await renderedComponent.update()
console.log('finished test for 200')
expect(renderedComponent.state('groceries').length).toEqual(2)
})
it('sets the state componentDidMount on error', async () => {
console.log('starting test for 500')
window.fetch = jest.fn().mockImplementation(() => Promise.resolve(
mockResponse(
400,
'Test Error',
JSON.stringify({ status: 400, statusText: 'Test Error!' })
)
))
const renderedComponent = await mount(<App />)
await renderedComponent.update()
console.log('finished test for 500')
expect(renderedComponent.state('errorStatus')).toEqual('Error fetching groceries')
})
})
})
When this runs, I receive this order of console logging (note that the test finishes and then it logs that state was set):
console.log src/App.test.js:22
starting test for 200
console.log src/App.js:10
calling fetch
console.log src/App.js:36
parse json
console.log src/App.test.js:39
finished test for 200
console.log src/App.js:42
setting state
I have created an example sandbox of my code:
This is abstracted a lot more in my apps, so changing the code itself is much more difficult (For example I want to test at a higher component, which has redux store, and this lower component calls the fetch, and sets the store eventually through a thunk).
How is this tested?
Upvotes: 3
Views: 2132
Reputation: 23705
I have no idea why await renderedComponent.update()
does not help you here(.update
does not return a Promise but it still means everything below comes as separated microtask).
But wrapping things into setTimeout(..., 0)
works for me. So it's difference between microtask and macrotask actually happens in some way.
it("sets the state componentDidMount on error", done => {
console.log("starting test for 500");
window.fetch = jest
.fn()
.mockImplementation(() =>
Promise.resolve(
mockResponse(
400,
"Test Error",
JSON.stringify({ status: 400, statusText: "Test Error!" })
)
)
);
const renderedComponent = mount(<App />);
setTimeout(() => {
renderedComponent.update();
console.log("finished test for 500");
expect(renderedComponent.state("errorStatus")).toEqual(
"Error fetching groceries"
);
done();
}, 0);
});
});
The only disadvantage of this approach: when expect()
fails it does not display failing message into Jest output. Jest is just complains on test have not finished in 5000 ms. In the same time valid error message like Expected value to equal: ...
goes to console.
Upvotes: 0
Reputation: 6869
Update method actually not returning promise that's why await is not working properly. To fix unit test you could move fetch call to another method and use that function from your test so that await works properly.
componentDidMount() {
console.log("calling fetch");
this.fetchCall();
}
fetchCall() {
return fetch("/api/v1/groceries")
.then(this.checkStatus)
.then(this.parseJSON)
.then(this.setStateFromData)
.catch(this.setError);
}
use instance() to access fetchCall method.
const renderedComponent = mount(<App />);
await renderedComponent.instance().fetchCall();
i have modified above changes in codesandbox: https://codesandbox.io/s/k38m6y89o7
Upvotes: 3