Testing a default export class method with Jest in Typescript

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.

jest.MockedClass

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.


jest.MockedFunction

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');
  });
});

jest.spyOn

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.

Answer №1

I enjoy developing a service using dynamic links (or other firebase functions) because it is simple to simulate.

dynamicLinksService.ts

import dynamicLinks from '@react-native-firebase/dynamic-links';

export const getInitialLink = () => dynamicLinks().getInitialLink();

useDynamicLink.ts

import { useEffect } from 'react';

import { navigateFromBackground } from '../deeplink';

import { getInitialLink } from './dynamicLinkService';

export const useDynamicLink = (): void => {
  useEffect(() => {
    getInitialLink().then((link) => {
      if (link && link.url) {
        navigateFromBackground(link.url);
      }
    });
  }, []);
};

useDynamicLink.test.ts

import { renderHook, act } from '@testing-library/react-hooks';

import { navigateFromBackground } from '../deeplink';

import { getInitialLink } from './dynamicLinkService';
import { useDynamicLink } from './useDynamicLink';

jest.mock('../deeplink', () => ({
  navigateFromBackground: jest.fn(),
}));

jest.mock('./dynamicLinkService', () => ({
  getInitialLink: jest.fn(),
}));

describe('Testing the use of Dynamic Links functionality', () => {
  it('should not trigger navigation when link is empty', async () => {
    const getInitialLinkMock = getInitialLink as jest.Mock;

    getInitialLinkMock.mockResolvedValue(null);

    await act(async () => {
      renderHook(() => useDynamicLink());
    });

    expect(navigateFromBackground).not.toHaveBeenCalled();
  });

  it('should trigger navigation when link exists', async () => {
    const getInitialLinkMock = getInitialLink as jest.Mock;

    getInitialLinkMock.mockResolvedValue({ url: 'www.google.com' });

    await act(async () => {
      renderHook(() => useDynamicLink());
    });

    expect(navigateFromBackground).toHaveBeenCalledWith('www.google.com');
  });
});

Answer №2

Your approach to jest.spyOn may need some refining.

When using Jest.spyOn, it's important to understand that it functions differently than traditional mocks. Jest.spyOn cleans up its mock within the scope where it is used and only becomes a 'spy' when you explicitly call mockImplementation on it. To efficiently manage changing mocks, consider using spyOn() and mocking the implementation in each test to avoid the repetitive task of clearing mocks every time. While both approaches can work effectively, focusing on attempt 3 would be beneficial.

Start by removing the dynamic links mock as we will spy on each specific test and implement the mock there instead.

Furthermore, since you are directly calling an exported function, ensure to import and spy on the function in this manner:

import * as dynamicLinks from '@react-native-firebase/dynamic-links';

const dynamicLinkSpy = jest.spyOn(dynamicLinks, 'dynamicLinks').mockImplentation( ... )

In this setup, dynamicLinks is the exported file being spied on, while dynamicLinks() represents the function called in the production code. Avoid adding .prototype and mimic how the production code calls it for effective testing. When replacing the implementation of dynamicLinks, create the return value that flows down to nested functions called within. Additionally, as your production code includes .then(), make sure to resolve a Promise within the function like so:

const dynamicLinkSpy = jest
  .spyOn(dynamicLinks, 'dynamicLinks')
  .mockImplementation(()=>{ return {getInitialLink: ()=> Promise.resolve('test')}} );

Experiment with different return values to anticipate various outcomes. Also, remember to verify if the function is indeed being called using expect(dynamicLinkSpy).toHaveBeenCalled();

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Utilize CountUp.js to generate a dynamic timer for tracking days and hours

I am looking to create a unique counter similar to the one featured on this website https://inorganik.github.io/countUp.js/ that counts up to a specific number representing hours. My goal is to display it in a format such as 3d13h, indicating days and hour ...

Is there a way to retrieve keys of the object from this combination type?

Can someone please help me understand how to retrieve keys from this union type? The Value is currently being assigned as a never type. I would like the Value to be either sno, key, or id type Key = { sno: number } | { key: number } | { id: number }; typ ...

Recursive Vue components can be implemented using typescript, allowing for

I am working on a TypeScript component that utilizes recursion: <template> <div :style="{ paddingLeft: depth * 20 + 'px' }"> <h1>Level {{ depth }}</h1> <div v-if="depth < 2"> &l ...

Detecting changes in Angular when the @Input() value remains the same

I have created an Angular Custom scroll directive that utilizes an @Input() to pass an HTML element as a parameter, allowing the scrollbar to move to that specific element. However, I've encountered an issue where if I pass the same HTML Element mult ...

What is the definition of a type that has the potential to encompass any subtree within an object through recursive processes?

Consider the data structure below: const data = { animilia: { chordata: { mammalia: { carnivora: { canidae: { canis: 'lupus', vulpes: 'vulpe' } } } } }, ...

An unexpected TypeScript error was encountered in the directory/node_modules/@antv/g6-core/lib/types/index.d.ts file at line 24, column 37. The expected type was

Upon attempting to launch the project post-cloning the repository from GitHub and installing dependencies using yarn install, I encountered an error. Updating react-scripts to the latest version and typescript to 4.1.2 did not resolve the issue. Node v: 1 ...

Examining Vuex Mutations using Jest confirms no alteration in state

I am currently facing an issue with testing a namespaced Vuex module using Jest. Despite making mutations to the mocked state, I am not seeing any changes reflected. Below is the code for my addEvents mutation: addEvents: (state, infos) => { t ...

Using TypeORM: Implementing a @JoinTable with three columns

Seeking assistance with TypeORM and the @JoinTable and @RelationId Decorators. Any help answering my question, providing a hint, or ideally solving my issue would be greatly appreciated. I am utilizing NestJS with TypeORM to create a private API for shari ...

What is the best way to assign JSON data to a Class variable within Angular?

In my code, I have a class called Projects export class Projects { project_id: number; project_name: string; category_id: number; project_type: string; start_date: Date; completion_date: Date; working_status: string; project_info: string; area: string; add ...

Are you interested in implementing the switcher function in a React JS class component?

I am having trouble implementing the switcher method in my react app, which is built using class components. Can you provide guidance on how to utilize the useThemeSwitcher() function in class components? How can I integrate this function into my web app ...

How can I verify the value of a class variable in TypeScript by using a method?

I need a more concise method to inform TypeScript that my client has been initialized (no longer null). While I have achieved this functionality, the current implementation seems unnecessarily verbose. Here is how it currently looks: export abstract class ...

What is the process for creating a new type from a nested part of an existing type?

Currently, my website is being developed with a focus on utilizing code generation to ensure type safety when handling GraphQl queries. Certain components within the application receive a portion of an object as a prop. The specific type structure is outli ...

"Null value is no longer associated with the object property once it has

What causes the type of y to change to string only after the destruction of the object? const obj: { x: string; y: string | null } = {} as any const { x, y } = obj // y is string now ...

What is the TypeScript definition for the return type of a Reselect function in Redux?

Has anyone been able to specify the return type of the createSelector function in Redux's Reselect library? I didn't find any information on this in the official documentation: https://github.com/reduxjs/reselect#q-are-there-typescript-typings ...

Enforcement of static methods in Typescript abstract classes is not mandatory

In my TypeScript code, I have a simple structure defined: abstract class Config { readonly NAME: string; readonly TITLE: string; static CoreInterface: () => any } class Test implements Config { readonly NAME: string; readonly TITL ...

Using a Jasmine spy to monitor an exported function in NodeJS

I've encountered difficulties when trying to spy on an exported function in a NodeJS (v9.6.1) application using Jasmine. The app is developed in TypeScript, transpiled with tsc into a dist folder for execution as JavaScript. Application Within my p ...

Troubleshooting compatibility issues between Sailsjs Services and TypeScript in Vscode

Having an issue with TypeScript in a Sails.js application. I am utilizing TypeScript to write my controller and attempting to use Sails.js services within the controllers. However, I encounter a syntax error in VSCODE. Below is the code snippet from MyCo ...

Jest tutorial: mocking constructor in a sub third-party attribute

Our express application uses a third-party module called winston for logging purposes. const express = require('express'); const app = express(); const { createLogger, transports } = require('winston'); const port = process.env.PORT | ...

Deselect the tick marks on the dropdown box for material selection

I need help implementing a function for a click event that will unselect all items. <mat-autocomplete [panelWidth]='290' panelClass="myPanelClass"> <mat-option *ngFor="let item of items" [value]="item.name&qu ...

TypeScript is unable to identify the data type of class members

I am working with a class called Colors: export class Colors { constructor( private domainColors: string[] = ['#F44336', '#FDB856', '#59CA08', '#08821C'], private numberRange: [number | string, number | string] = [-1 ...