Search code examples
react-nativejestjsreact-hooksreact-hooks-testing-library

Mock Linking.openURL in React Native it's never been called


I´m writing some tests for my app and I´m trying to mock Linking module. I'm using jest. The Linking.canOpenURL mock it's working fine (toHaveBeenCalled is returning true), but openURL mock is never called.

    function mockSuccessLinking() {
      const canOpenURL = jest
        .spyOn(Linking, 'canOpenURL')
        .mockImplementation(() => Promise.resolve(true));
      const openURL = jest
        .spyOn(Linking, 'openURL')
        .mockImplementation(() => Promise.resolve(true));

      return { canOpenURL, openURL };
    }

The problem is that openURL is not been called.

Here is the test:

test('should open url when there is a proper app the open it', async () => {
      const { canOpenURL, openURL } = mockSuccessLinking();
      const { result } = renderHook(() =>
         useApplyToJob('https://www.google.com/'),
      );
      const [apply] = result.current;

      // Act
      apply();

      // Assert
      expect(result.current[1].error).toBeNull();
      expect(canOpenURL).toHaveBeenCalled();
      expect(openURL).toHaveBeenCalled();
});

And this the hook under test:

export function useApplyToJob(url) {
  const [error, setError] = useState(null);

  const apply = () => {
    Linking.canOpenURL(url).then(supported => {
      if (supported) {
        Linking.openURL(url);
      } else {
        setError(`Don't know how to open ${url}`);
      }
    });
  };

  return [apply, { error }];
}

Solution

  • Given canOpenURL returns a promise, you'll need to wait for the async to occur before testing if openURL has been called. react-hooks-testing-library ships a few async utils to help with this.

    Generally it's preferred to use waitForNextUpdate or waitForValueToChange as they are a bit more descriptive of what the test is waiting for, but your hook is not updating any state in the successful case, so you will need to use the more general waitFor utility instead:

    test('should open url when there is a proper app the open it', async () => {
          const { canOpenURL, openURL } = mockSuccessLinking();
          const { result, waitFor } = renderHook(() =>
             useApplyToJob('https://www.google.com/'),
          );
          const [apply] = result.current;
    
          // Act
          apply();
    
          // Assert
          expect(result.current[1].error).toBeNull();
          expect(canOpenURL).toHaveBeenCalled();
    
          await waitFor(() => {
              expect(openURL).toHaveBeenCalled();
          });
    });
    

    As a side note, destructuring result.current to access apply is not recommended. It may work now, but it does not take much refactoring before the apply you're calling is using stale values from a previous render.

    Similarly, I'd recommend wrapping the apply() call in act, even though it does not update any state right now. It just makes refactoring easier in the future as well as keeping your tests more consistent when you're testing the error case (which will need an act call).

    import { renderHook, act } from '@testing-library/react-hooks';
    
    // ...
    
    test('should open url when there is a proper app the open it', async () => {
          const { canOpenURL, openURL } = mockSuccessLinking();
          const { result, waitFor } = renderHook(() =>
             useApplyToJob('https://www.google.com/'),
          );
    
          // Act
          act(() => {
            result.current[0]();
          });
    
          // Assert
          expect(result.current[1].error).toBeNull();
          expect(canOpenURL).toHaveBeenCalled();
    
          await waitFor(() => {
              expect(openURL).toHaveBeenCalled();
          });
    });