Nayaro
Nayaro

Reputation: 147

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:

It can't be much, but can someone tell me which piece I am missing?

Upvotes: 1

Views: 2673

Answers (1)

skyboyer
skyboyer

Reputation: 23705

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:

  1. 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)
  2. initialize element(I mean just run shallow() or mount())
  3. await till your mocks are resolved(with extra await, using setTimeout(... ,0) or flush-promises will work, check how microtasks/macrotasks works)
  4. assert against element's render and check if your mocks has been called

And finally:

  1. setting state directly
  2. mocking/spying on internal methods
  3. 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);
})

Upvotes: 1

Related Questions