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 automock
jest.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.