Scenario:
I have a custom hook that relies on
@react-native-firebase/dynamic-links
for testing. We are using @testing-library
and its utilities for react-native to test hooks (@testing-library/react-hooks
).
Below is the simplified example of the hook I want to test:
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 need the getInitialLink
call to return a specific value in each individual test. While I was able to mock getInitialLink
with jest.mock(...)
, it applied the same mock for all tests, as the method I wanted 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: Constructors cannot be mocked 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: Using act wrapper to ensure all events are handled before
// inspecting the state by the test.
await act(async () => {
renderHook(() => useDynamicLink());
});
expect(navigateFromBackground).toHaveBeenCalledWith('fake-link');
});
});
Challenges Faced:
While this approach worked, I encountered difficulty in altering the return value for each test. Even though Jest provides various ways to mock dependencies, I struggled to achieve the desired outcome.
Firebase exports a wrapped class by default, making it challenging to effectively mock it.
declare const defaultExport: ReactNativeFirebase.FirebaseModuleWithStatics<
FirebaseDynamicLinksTypes.Module,
FirebaseDynamicLinksTypes.Statics
>;
According to the documentation, mocking should be done as shown below:
import dynamicLinks from '@react-native-firebase/dynamic-links';
const dynamicLinksMock = dynamicLinks as jest.MockedClass<typeof dynamicLinks>;
However, it resulted in 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'.
This is because the object is recognized as a non-class entity due to the wrapping.
As an alternative, I attempted to mock it using a function instead of arrow functions. Although this approach provided progress, I faced challenges in specifying multiple properties. After numerous attempts, automocking most of these properties seemed unmanageable.
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 () => {
dynamicLinksMock.mockImplementationOnce(function () {
return {
buildLink: jest.fn(),
buildShortLink: jest.fn(),
...
getInitialLink: async () => ({
minimumAppVersion: '123',
utmParameters: { 'fake-param': 'fake-value' },
url: 'fake-link',
}),
};
});
await act(async () => {
renderHook(() => useDynamicLink());
});
expect(navigateFromBackground).toHaveBeenCalledWith('fake-link');
});
});
Lastly, I tried to utilize spyOn
, which seemed like a suitable choice given its ability to mock specific functions only. However, attempting this approach led to a runtime error during test execution.
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 Encountered:
Cannot spy the getInitialLink property because it is not a function; undefined given instead.
In conclusion, I am seeking guidance on how to mock the getInitialLink
method effectively. Any advice or tips would be greatly appreciated!
Edit 1:
Following the suggestion by @user275564, I attempted the following:
jest.spyOn(dynamicLinks, 'dynamicLinks').mockImplementation(() => {
return { getInitialLink: () => Promise.resolve('fake-link') };
});
Unfortunately, Typescript failed to compile due to the 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 could only include the static properties in the object, as seen here: https://i.sstatic.net/b6QM7.png
Hence, I resorted to using the suggested dynamicLinks.prototype
approach mentioned in this answer.