Jest: Wait For Called .then Of Async Method In ComponentDidMount
I am currently stuck on writing a test of my React-App.
I have an async call in my componentDidMount method and are updating the state after returning. However, I do not get this to work.
I have found several solutions and None seems to work as expected. Below is the nearest point I have come to.
App:
class App extends Component<{}, IState> {
state: IState = {
fetchedData: false
};
async componentDidMount() {
await thing.initialize();
this.test();
}
test = () => {
this.setState({ fetchedData: true })
};
render() {
return this.state.fetchedData ? (
<div>Hello</div>
) : (
<Spinner />
);
}
}
The test
it('Base test for app', async () => {
const spy = spyOn(thing, 'initialize').and.callThrough(); // just for debugging
const wrapper = await mount(<App />);
// await wrapper.instance().componentDidMount(); // With this it works, but componentDidMount is called twice.
wrapper.update();
expect(wrapper.find('Spinner').length).toBe(0);
});
Well, so...thing.initialize is called (it is an async method that fetches some stuff). If I do explicitly call wrapper.instance().componentDidMount() then it will work, but componentDidMount will be called twice.
Here are my ideas that I have tried but None succeeded:
- Spying on thing.initialize() -> I did not find out how I proceed with the test after the method has been called and finished.
- Spying on App.test -> The same here
- Working with promises instead of async await
- At the beginning, I had an thing.initialize().then(this.test) in my componentDidMount
It can't be much, but can someone tell me which piece I am missing?
Answer
if this is integration test you better to follow awaiting approach that say Selenium use: that is, just wait until some element appears or timeout reached. How it should be coded depends on library you use(for Puppeter it should be waitForSelector).
Once it's about unit test then I suggest you different approach:
- mock every single external dependencies with
Promise
you control(by your code it's hard to say if automatic mock will work or you need to compose mock factory but one of them or both will help) - initialize element(I mean just run
shallow()
ormount()
) - await till your mocks are resolved(with extra
await
, usingsetTimeout(... ,0)
orflush-promises
will work, check how microtasks/macrotasks works) - assert against element's
render
and check if your mocks has been called
And finally:
- setting state directly
- mocking/spying on internal methods
- verifying against state
are all lead to unstable test since it's implementation details you should not worry about during unit-testing. And it's hard to work with them anyway.
So your test would look like:
import thing from '../thing';
import Spinner from '../../Spinner';
import flushPromises from 'flush-promises';
it('loads data and renders it', async () => {
jest.mock('../thing'); // thing.implementation is already mocked with jest.fn()
thing.initialize.mockReturnValue(Promise.resolve(/*data you expect to return*/));
const wrapper = shallow(<App />);
expect(wrapper.find(Spinner)).toHaveLength(1);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(0);
await flushPromises();
expect(wrapper.find(Spinner)).toHaveLength(0);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(1);
})
or you may test how component behaves on rejection:
import thing from '../thing';
import Spinner from '../../Spinner';
import flushPromises from 'flush-promises';
it('renders error message on loading failuer', async () => {
jest.mock('../thing'); // thing.implementation is already mocked with jest.fn()
thing.initialize.mockReturnValue(Promise.reject(/*some error data*/));
const wrapper = shallow(<App />);
expect(wrapper.find(Spinner)).toHaveLength(1);
await flushPromises();
expect(wrapper.find(Spinner)).toHaveLength(0);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(0);
expect(wrapper.find(SomeErrorMessage)).toHaveLength(1);
})
Related Questions
- → Import statement and Babel
- → should I choose reactjs+f7 or f7+vue.js?
- → Uncaught TypeError: Cannot read property '__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED' of undefined
- → .tsx webpack compile fails: Unexpected token <
- → React-router: Passing props to children
- → ListView.DataSource looping data for React Native
- → React Native with visual studio 2015 IDE
- → Can't test submit handler in React component
- → React + Flux - How to avoid global variable
- → Webpack, React & Babel, not rendering DOM
- → How do I determine if a new ReactJS session and/or Browser session has started?
- → Alt @decorators in React-Native
- → How to dynamically add class to parent div of focused input field?