How to test async function with spyOn?
Asked Answered
L

3

12

I am trying to test an async function in a react native app.

class myClass extends React.Component {

  ...

  closeModal = async () => {

    if (someCondition) {
      await myFunction1();
    } else {
      await myFunction2();
    }

    this.props.navigation.state.params.onGoBack();
    this.props.navigation.navigate('Main');
  };

  ...

}

This is my test:

const navigation = {
  navigate: jest.fn(),
  state: { params: { onGoBack: jest.fn() } },
};

const renderComponent = overrides => {
  props = {
    navigation,
    ...overrides,
  };

  return shallow(< myClass.wrappedComponent {...props} />);
};


describe('When the user presses the close icon', () => {
    it('should close the modal', () => {
      const wrapper = renderComponent();
      const instance = wrapper.instance();
      const spyCloseModal = jest.spyOn(instance, 'closeModal');
      instance().forceUpdate();
      component
        .find({ testID: 'close-icon' })
        .props()
        .onPress();
      expect(spyCloseModal).toHaveBeenCalled(); // this is passed
      expect(navigation.navigate).toHaveBeenCalled(); // this is not passed
    });
});

It looks like it gets stuck on the await calls. If I remove the await calls then it passes. Someone mentioned in another post to use .and.callThrough after spyOn but it gives me this error

Cannot read property 'callThrough' of undefined

Leboff answered 18/6, 2019 at 18:16 Comment(0)
P
7

one of solution is to make your test async and run await (anything) to split your test into several microtasks:

it('should close the modal', async () => {
      const wrapper = renderComponent();
      component
        .find({ testID: 'close-icon' })
        .props()
        .onPress();
      await Promise.resolve();
      expect(navigation.state.params.onGoBack).toHaveBeenCalled(); 
      expect(navigation.navigate).toHaveBeenCalledWith("Main");
    });

I believe you don't need either .forceUpdate nor .spyOn on instance method. once navigation happens properly it does not matter by what internal method it has been called

more on microtask vs macrotask: https://abc.danch.me/microtasks-macrotasks-more-on-the-event-loop-881557d7af6f

alternative is to use macrotask(setTimeout(...., 0))

it('should close the modal', (done) => {
      const wrapper = renderComponent();
      component
        .find({ testID: 'close-icon' })
        .props()
        .onPress();
      setTimeout(() => {
        expect(navigation.state.params.onGoBack).toHaveBeenCalled(); 
        expect(navigation.navigate).toHaveBeenCalledWith("Main");
        done();
    });
}
Putup answered 18/6, 2019 at 19:41 Comment(0)
F
1

Yes, you're on the right track...the issue is that closeModal is asynchronous.

The await hasn't finished by the time execution returns to the test so this.props.navigation.navigate hasn't been called yet.

The test needs to wait for closeModal to complete before asserting that navigate has been called.

closeModal is an async function so it will return a Promise...

...and you can use the spy to retrieve the Promise it returns...

...then you can call await on that Promise in your test to make sure closeModal has completed before asserting that navigate has been called.

Here is a simplified working example to get you started:

import * as React from 'react';
import { shallow } from 'enzyme';

class MyClass extends React.Component {
  closeModal = async () => {
    await Promise.resolve();
    this.props.navigation.navigate('Main');
  }
  render() { return <div onClick={() => this.closeModal()}></div> }
}

test('MyClass', async () => {  // <= async test function
  const props = { navigation: { navigate: jest.fn() }};
  const wrapper = shallow(<MyClass {...props} />);
  const instance = wrapper.instance();
  const spyCloseModal = jest.spyOn(instance, 'closeModal');
  wrapper.find('div').simulate('click');
  expect(spyCloseModal).toHaveBeenCalled();  // Success!
  const promise = spyCloseModal.mock.results[0].value;  // <= get the Promise returned by closeModal
  await promise;  // <= await the Promise
  expect(props.navigation.navigate).toHaveBeenCalled();  // Success!
})

Note the use of mockFn.mock.results to get the Promise returned by closeModal.

Flied answered 21/6, 2019 at 1:39 Comment(0)
E
0
it('myFunction', async ()=> {
    spyOn(object, 'myFunction').and.returnValue(Promise.resolve('mockedResult'));
    const result = await foo();
    expect(result).toBe('mockedResult');
})
Ellene answered 17/4, 2023 at 11:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.