Hur man mockar React Navigations navigation-prop för enhetstester med TypeScript i React Native?


Jag bygger en React Native-app med TypeScript. För min navigering använder jag React Navigation och för mina enhetstester använder jag Jest och Enzyme.

Här är den (nedskalade) koden för en av mina skärmar (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;

När jag försökte testa skärmarna stötte jag på ett problem. Skärmarna förväntar sig en prop med en typ av NavigiationScreenProps eftersom jag använder React Navigations navigationsprop. Här är kod för testfilen (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);
    });
  });
});

Problemet är att LoadingScreen förväntar sig en navigation-prop.

Jag får felet:

[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

Hur kan jag fixa detta?

Jag tror att jag på något sätt måste mocka navigation-prop. Jag försökte göra det (som du kan se importerade jag * från React Navigation i mitt test), men kunde inte lista ut hur. Det finns bara NavigationActions som är åtminstone något användbar men den inkluderar bara navigate(). TypeScript förväntar sig att allt, även state, ska mockas. Hur kan jag mocka navigation-prop?

Redigering 1: Är tillvägagångssättet att använda NavigationScreenProps ens korrekt eller borde jag använda gränssnittets Props-metod? Om ja, hur skulle du då mocka det (det resulterar i samma fel).

Redigera 2: Använder det andra tillvägagångssättet med gränssnittet och

export class LoadingScreen extends Component<Props, object>

Jag kunde "lösa" det här problemet. Jag var bokstavligen tvungen att mocka varje enskild egenskap av navigeringsobjektet på det här sättet:

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

Frågan kvarstår: Är detta korrekt? Eller finns det en bättre lösning?

Redigera 3: När jag brukade använda JS räckte det med att mocka bara den egenskap jag behövde (ofta bara navigate). Men sedan jag började använda TypeScript var jag tvungen att mocka varje enskild aspekt av navigation. Annars skulle TypeScript klaga på att komponenten förväntar sig en prop med en annan typ.

Svar #1

Problem

Mocken matchar inte den förväntade typen, så TypeScript rapporterar ett fel.

Lösning

Du kan använda typen any "för att avstå från typkontroll och låta värdena passera genom kompileringstidskontroller".

Detaljer

Som du nämnde fungerar det i JavaScript att bara mocka det som behövs för testet.

I TypeScript kommer samma mock att orsaka ett fel eftersom det inte helt överensstämmer med den förväntade typen.

I situationer som dessa där du har en mock som du vet inte matchar den förväntade typen kan du använda any för att tillåta mocken att passera kompileringstidskontroller.

Här är ett uppdaterat 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
    });
  });
});

Svar #2

För Typescript-användare, ett alternativt tillvägagångssätt bara för intressets skull.

För de som inte gillar att använda någon, här är en lösning som använder Typescripts Partial-verktygstyp och typändringar (med '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}/>);
    });
});

Genom att använda Partial njuter vi av våra editor/IDE intellisense-funktioner, type-säkerställningen 'as' som används i navigation prop-definitionen kan ersättas med any, jag gillar bara inte att använda any om jag inte behöver.

Du kan bygga vidare på grunderna i denna kodsnutt för det specifika scenariot som beskrivs i frågan.

Relaterade artiklar