50

I'm building a React Native app with TypeScript. For my navigation I use React Navigation and for my unit testing I use Jest and Enzyme.

Here is the (stripped down) code for one of my screen (LoadingScreen.tsx):

import styles from "./styles";
import React, { Component } from "react";
import { Text, View } from "react-native";
import { NavigationScreenProps } from "react-navigation";

// Is this correct?
export class LoadingScreen extends Component<NavigationScreenProps> {
// Or should I've done:
// export interface Props {
//   navigation: NavigationScreenProp<any, any>;
// }

// export class LoadingScreen extends Component<Props> {
  componentDidMount = () => {
    this.props.navigation.navigate("LoginScreen");
  };

  render() {
    return (
      <View style={styles.container}>
        <Text>This is the LoadingScreen.</Text>
      </View>
    );
  }
}

export default LoadingScreen;

When trying to test the screens I came across a problem. The screens expects a prop with a type of NavigiationScreenProps because I'm accessing React Navigations navigation prop. Here is the testing file's code (LoadingScreen.test.tsx):

import { LoadingScreen } from "./LoadingScreen";
import { shallow, ShallowWrapper } from "enzyme";
import React from "react";
import { View } from "react-native";
import * as navigation from "react-navigation";

const createTestProps = (props: Object) => ({
  ...props
});

describe("LoadingScreen", () => {
  describe("rendering", () => {
    let wrapper: ShallowWrapper;
    let props: Object;
    beforeEach(() => {
      props = createTestProps({});
      wrapper = shallow(<LoadingScreen {...props} />);
    });

    it("should render a <View />", () => {
      expect(wrapper.find(View)).toHaveLength(1);
    });
  });
});

The problem is, that LoadingScreen expects a navigation prop.

I get the error:

[ts]
Type '{ constructor: Function; toString(): string; toLocaleString(): string; valueOf(): Object; hasOwnProperty(v: string | number | symbol): boolean; isPrototypeOf(v: Object): boolean; propertyIsEnumerable(v: string | ... 1 more ... | symbol): boolean; }' is not assignable to type 'Readonly<NavigationScreenProps<NavigationParams, any>>'.
  Property 'navigation' is missing in type '{ constructor: Function; toString(): string; toLocaleString(): string; valueOf(): Object; hasOwnProperty(v: string | number | symbol): boolean; isPrototypeOf(v: Object): boolean; propertyIsEnumerable(v: string | ... 1 more ... | symbol): boolean; }'.
(alias) class LoadingScreen

How can I fix this?

I think I somehow have to mock the navigation prop. I tried doing that (as you can see I imported * from React Navigation in my test), but couldn't figure out. There is only NavigationActions that is remotely useful but it only includes navigate(). TypeScript expects everything, even the state, to be mocked. How can I mock the navigation prop?

Edit 1: Is the approach of using NavigationScreenProps even correct or should I use the interface Props approach? If yes how would you then mock than (it results in the same error).

Edit 2: Using the second approach with the interface and

export class LoadingScreen extends Component<Props, object>

I was able to "solve" this problem. I literally had to mock every single property of the navigation object like this:

const createTestProps = (props: Object) => ({
  navigation: {
    state: { params: {} },
    dispatch: jest.fn(),
    goBack: jest.fn(),
    dismiss: jest.fn(),
    navigate: jest.fn(),
    openDrawer: jest.fn(),
    closeDrawer: jest.fn(),
    toggleDrawer: jest.fn(),
    getParam: jest.fn(),
    setParams: jest.fn(),
    addListener: jest.fn(),
    push: jest.fn(),
    replace: jest.fn(),
    pop: jest.fn(),
    popToTop: jest.fn(),
    isFocused: jest.fn()
  },
  ...props
});

The question remains: Is this correct? Or is there a better solution?

Edit 3: Back when I used JS, it was enough to mock only the property I needed (often just navigate). But since I started using TypeScript, I had to mock every single aspects of navigation. Otherwise TypeScript would complain that the component expects a prop with a different type.

J. Hesters
  • 13,117
  • 31
  • 133
  • 249

5 Answers5

57

Issue

The mock does not match the expected type so TypeScript reports an error.

Solution

You can use the type any "to opt-out of type-checking and let the values pass through compile-time checks".

Details

As you mentioned, in JavaScript it works to mock only what is needed for the test.

In TypeScript the same mock will cause an error since it does not completely match the expected type.

In situations like these where you have a mock that you know does not match the expected type you can use any to allow the mock to pass through compile-time checks.


Here is an updated test:

import { LoadingScreen } from "./LoadingScreen";
import { shallow, ShallowWrapper } from "enzyme";
import React from "react";
import { View } from "react-native";

const createTestProps = (props: Object) => ({
  navigation: {
    navigate: jest.fn()
  },
  ...props
});

describe("LoadingScreen", () => {
  describe("rendering", () => {
    let wrapper: ShallowWrapper;
    let props: any;   // use type "any" to opt-out of type-checking
    beforeEach(() => {
      props = createTestProps({});
      wrapper = shallow(<LoadingScreen {...props} />);   // no compile-time error
    });

    it("should render a <View />", () => {
      expect(wrapper.find(View)).toHaveLength(1);   // SUCCESS
      expect(props.navigation.navigate).toHaveBeenCalledWith('LoginScreen');   // SUCCESS
    });
  });
});
Brian Adams
  • 43,011
  • 9
  • 113
  • 111
  • 1
    Nice [tutorial](https://medium.com/@jan.hesters/3-easy-steps-to-set-up-react-native-with-typescript-jest-and-enzyme-592ca042262f), btw. I used it to set up the environment to look at this question – Brian Adams Oct 03 '18 at 03:23
  • 1
    Thank you! It's funny because, I remember you helping me out with some other critical question I had. Gonna spread your knowledge for you! – J. Hesters Oct 04 '18 at 08:29
  • 3
    lol, sounds good. The knowledge isn't really "mine" so spread it around...knowledge works best when it's shared – Brian Adams Oct 07 '18 at 04:16
  • So what about functional components? – Miguel Coder Oct 01 '20 at 11:52
6

For Typescript users, an alternative approach just for interests sake.

For those that don't like using any, here's a solution that utilises Typescript's Partial utility type and Type assertions (using 'as')

import { NavigationScreenProp } from "react-navigation";
import SomeComponent from "./SomeComponent";

type NavigationScreenPropAlias = NavigationScreenProp<{}>;

describe("Some test spec", () => {
    let navigation: Partial<NavigationScreenPropAlias>;
    beforeEach(() => {
        navigation = {
            dispatch: jest.fn()
        }
    });

    test("Test 1", () => {
        const {} = render(<SomeComponent navigation={navigation as NavigationScreenPropAlias}/>);
    });
});

Using Partial, we enjoy our editor/IDE intellisense features, the type assertion 'the 'as' used in the navigation prop definition could be replaced with an any, I just don't like using any if I don't have to.

You could build on the fundamentals in this code snippet to the specific scenario laid out in the question.

Lee Brindley
  • 6,242
  • 5
  • 41
  • 62
3

Hi you have several options:

1 - If you are using the useNavigation hook:

Example:

const navigation = useNavigation();
const goBack = () => {
        if (navigation.canGoBack()) { navigation.goBack();}
    };
  • 1.1 - Import this: import * as Navigation from "@react-navigation/native";

  • 1.2 - Before the render method of the test. Obs the canGoBack can be navigate, goBack or the method you want

    const mockedNavigation = jest.fn();
    jest.spyOn(Navigation, "useNavigation").mockReturnValue({ canGoBack: mockedNavigation });
    

...

  • 1.3 Check if the method fires:

    fireEvent.press(button or the element you want);
    
    expect(mockedNavigation).toHaveBeenCalled();
    

2 You are passing the navigation as props

  • 2.1 Create a mocked navigator to check that the function is triggered. Obs: put in the json the methods you need

    const mockedCanGoBack = jest.fn().mockReturnValue(true);
    
    const mockedGoBack = jest.fn();
    
    const mockedNavigation = {
        canGoBack: mockedCanGoBack,
        goBack: mockedGoBack,
    }
    
  • Pass the navigation prop mockedNavigation with the as any. The any will be the typescript work.

    const screen = render(<ElementYouWant navigation={mockedNavigation as any} ...> ... </ElementYouWant>);
    
  • 2.3 Fire event and check that funcion is fired. Obs is it necessary that you mock all the functions you need.

Ishita Sinha
  • 2,168
  • 4
  • 25
  • 38
1

I'm not super happy with my solution, but I've started mocking only the properties of Navigation that are needed for a specific component, for example:

export function UserHomeScreen({
    navigation,
}: {
    navigation: {
        goBack: () => void;
        navigate: NavigationFunction<UserStackParams, "AddPhoneNumber" | "ValidatePhoneNumber">;
        setOptions: (options: { title: string }) => void;
    };
})

In the test, I can provide these as mocks:

const goBack = jest.fn();
const navigate = jest.fn();
const setOptions = jest.fn();

renderer.create(<UserHomeScreen navigation={ { goBack, navigate, setOptions } } />)

The definition of NavigationFunction is a mouthful as well:

export type NavigationFunction<ParamsList, Routes extends keyof ParamsList> = <T extends Routes>(
    target: Routes,
    params?: ParamsList[T]
) => void;

I've not been able to come up with a correct signature for Navigator's functions addListener and removeListener

Johannes Brodwall
  • 7,673
  • 5
  • 34
  • 28
0

Here's a workaround to preserve type inference in your code, inspired by a solution provided by Brain Adams.

Imports

Start by importing the necessary modules and components:

import React from "react";
import { LoadingScreen } from "./LoadingScreen";
import { shallow, ShallowWrapper } from "enzyme";
import { View } from "react-native";
import {
  NativeStackNavigationProp,
  NativeStackScreenProps,
} from "@react-navigation/native-stack";
import { RouteProp } from "@react-navigation/core/lib/typescript/src/types";

Define types for route configurations and navigation props

Define the types for the route configurations and navigation props:

type RootStackParamList = {
  LoginScreen: undefined;
};

type LoginProps = NativeStackScreenProps<RootStackParamList, "LoginScreen">;

type NavigationType = NativeStackNavigationProp<
  RootStackParamList,
  "LoginScreen",
  undefined
>;

type RouteType = RouteProp<RootStackParamList, "LoginScreen">;

Update createTestProps function to include new types

Extend the createTestProps function with the new types you have defined:

const createTestProps = (): unknown & LoginProps => ({
  navigation: {
    navigate: jest.fn(),
  } as unknown as NavigationType,
  route: jest.fn() as unknown as RouteType,
});

Use the types in your unit test

Now, you can use the types in your unit test:

describe("LoadingScreen", () => {
  describe("rendering", () => {
    let wrapper: ShallowWrapper;
    let props: unknown & LoginProps;
    beforeEach(() => {
      props = createTestProps();
      wrapper = shallow(<LoadingScreen {...props} />); // no compile-time error
    });

    it("should render a <View />", () => {
      expect(wrapper.find(View)).toHaveLength(1); // SUCCESS
      expect(props.navigation.navigate).toHaveBeenCalledWith("LoginScreen"); // SUCCESS
    });
  });
});

By using these types, you can ensure that your code is type-safe and can catch errors at compile-time instead of run-time. This will make your code more reliable and easier to maintain.