Context
I would like to test a custom hook, that depends on @react-native-firebase/dynamic-links
. We are using @testing-library
for react-native and its utility functions to test hooks (@testing-library/react-hooks
).
This is the hook I would like to test (this is a simplified example):
import { useEffect } from 'react';import dynamicLinks from '@react-native-firebase/dynamic-links';import { navigateFromBackground } from '../deeplink';// Handles dynamic link when app is loaded from closed state.export const useDynamicLink = (): void => { useEffect(() => { void dynamicLinks() .getInitialLink() .then((link) => { if (link && link.url) { navigateFromBackground(link.url); } }); }, []);};
I would like the getInitialLink
call to return something in each separate test. I have been able to mock getInitialLink
with jest.mock(...)
, however this mocks it for all tests. I think the trouble is that the method I would like to mock, is a method on a class.
import { useDynamicLink } from './useDynamicLink';import { renderHook, act } from '@testing-library/react-hooks';import { navigateFromBackground } from '../deeplink';jest.mock('../deeplink');// IMPORTANT: You cannot mock constructors with arrow functions. New cannot be// called on an arrow function.jest.mock('@react-native-firebase/dynamic-links', () => { return function () { return { getInitialLink: async () => ({ url: 'fake-link', }), }; };});describe('tryParseDynamicLink', () => { it('should return null if url is empty', async () => { // IMPORTANT: act wrapper is needed so that all events are handled before // state is inspected by the test. await act(async () => { renderHook(() => useDynamicLink()); }); expect(navigateFromBackground).toHaveBeenCalledWith('fake-link'); });});
Attempts
So this works, but I am not able to change the return value for each test. Jest offers a wide variety of ways to mock dependencies, however I was not able to make it work.
Firebase exports by default a class, but the class itself is wrapped.
declare const defaultExport: ReactNativeFirebase.FirebaseModuleWithStatics< FirebaseDynamicLinksTypes.Module, FirebaseDynamicLinksTypes.Statics>;
According to the documentation, you would need to mock it like described below.
import dynamicLinks from '@react-native-firebase/dynamic-links';const dynamicLinksMock = dynamicLinks as jest.MockedClass<typeof dynamicLinks>;
It however throws the following error:
Type 'FirebaseModuleWithStatics<Module, Statics>' does not satisfy the constraint 'Constructable'. Type 'FirebaseModuleWithStatics<Module, Statics>' provides no match for the signature 'new (...args: any[]): any'.
Effectively it does not recognise it as a class since it is wrapped.
I decided to then mock it by using a function (and not using arrow functions). With this approach I was able to get a lot further, however with this approach I need to provide all properties. I attempted this for a while, but I gave up after adding X amount of properties (see code snippet below). So if this is the way to go, I would like to know how to automock most of this.
import { useDynamicLink } from './useDynamicLink';import { renderHook, act } from '@testing-library/react-hooks';import { navigateFromBackground } from '../deeplink';import dynamicLinks from '@react-native-firebase/dynamic-links';const dynamicLinksMock = dynamicLinks as jest.MockedFunction< typeof dynamicLinks>;jest.mock('../deeplink');describe('tryParseDynamicLink', () => { it('should return null if url is empty', async () => { // eslint-disable-next-line prefer-arrow-callback dynamicLinksMock.mockImplementationOnce(function () { return { buildLink: jest.fn(), buildShortLink: jest.fn(), app: { options: { appId: 'fake-app-id', projectId: 'fake-project-id', }, delete: jest.fn(), utils: jest.fn(), analytics: jest.fn(), name: 'fake-name', crashlytics: jest.fn(), dynamicLinks: jest.fn(), }, onLink: jest.fn(), resolveLink: jest.fn(), native: jest.fn(), emitter: jest.fn(), getInitialLink: async () => ({ minimumAppVersion: '123', utmParameters: { 'fake-param': 'fake-value' }, url: 'fake-link', }), }; }); await act(async () => { renderHook(() => useDynamicLink()); }); expect(navigateFromBackground).toHaveBeenCalledWith('fake-link'); });});
The last attempt was to use spyOn
which seems fitting in this case. Since it will mock only specific functions, however this throws a runtime error when I try to run the tests.
import { useDynamicLink } from './useDynamicLink';import { renderHook, act } from '@testing-library/react-hooks';import { navigateFromBackground } from '../deeplink';import dynamicLinks from '@react-native-firebase/dynamic-links';jest.mock('../deeplink');// Ensure automockjest.mock('@react-native-firebase/dynamic-links');describe('tryParseDynamicLink', () => { it('should return null if url is empty', async () => { jest .spyOn(dynamicLinks.prototype, 'getInitialLink') .mockImplementationOnce(async () => 'test'); await act(async () => { renderHook(() => useDynamicLink()); }); expect(navigateFromBackground).toHaveBeenCalledWith('fake-link'); });});
Error:
Cannot spy the getInitialLink property because it is not a function; undefined given instead
So all in all I am at a complete loss on how to mock the getInitialLink
method. If anyone could provide any advice or tips it would be greatly appreciated!
Edit 1:
Based on the advice of @user275564 I tried the following:
jest.spyOn(dynamicLinks, 'dynamicLinks').mockImplementation(() => { return { getInitialLink: () => Promise.resolve('fake-link') };});
Unfortunately typescript does not compile because of the following error:
No overload matches this call. Overload 1 of 4, '(object: FirebaseModuleWithStatics<Module, Statics>, method: never): SpyInstance<never, never>', gave the following error. Argument of type 'string' is not assignable to parameter of type 'never'. Overload 2 of 4, '(object: FirebaseModuleWithStatics<Module, Statics>, method: never): SpyInstance<never, never>', gave the following error. Argument of type 'string' is not assignable to parameter of type 'never'.
I am only able to put forth the static properties on the object there which are:
This is why I went for the dynamicLinks.prototype
which was suggested in this answer.