Hvordan kan man lage en mock av React Navigations navigation-prop for enhetstester med TypeScript i React Native?


Jeg bygger en React Native-app med TypeScript. For navigasjonen min bruker jeg React Navigation, og for enhetstesting bruker jeg Jest og Enzyme.

Her er den (avkledde) koden for en av mine skjermer (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 jeg prøvde å teste skjermene støtte jeg på et problem. Skjermene forventer en prop med typen NavigationScreenProps fordi jeg får tilgang til React Navigations navigation prop. Her er koden til 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 er at LoadingScreen forventer en navigation-prop.

Jeg får feilen:

[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

Hvordan kan jeg fikse dette?

Jeg tror jeg på en eller annen måte må mocke navigation-propen. Jeg prøvde å gjøre det (som du kan se importerte jeg * fra React Navigation i testen min), men klarte ikke å finne ut av det. Det finnes bare NavigationActions som er noe nyttig, men den inkluderer bare navigate(). TypeScript forventer at alt, til og med staten, må mockes. Hvordan kan jeg mocke navigation-propen?

Rediger 1: Er tilnærmingen med å bruke NavigationScreenProps i det hele tatt korrekt, eller burde jeg bruke interface Props-tilnærmingen? Hvis ja, hvordan ville du da mocke det (det resulterer i samme feil).

Rediger 2: Bruke den andre tilnærmingen med grensesnittet og

export class LoadingScreen extends Component<Props, object>

Jeg klarte å "løse" dette problemet. Jeg måtte bokstavelig talt mocke hver eneste egenskap ved navigasjonsobjektet slik:

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

Spørsmålet gjenstår: Er dette riktig? Eller finnes det en bedre løsning?

Redigering 3: Da jeg brukte JS, var det nok til å mocke bare den egenskapen jeg trengte (ofte bare navigere). Men siden jeg begynte å bruke TypeScript, måtte jeg mocke alle aspekter av navigasjonen. Ellers ville TypeScript klage på at komponenten forventer en prop med en annen type.

Svar #1

Problem

Mocken samsvarer ikke med den forventede typen, så TypeScript rapporterer en feil.

Løsning

Du kan bruke typen any "for å velge bort typetesting og la verdiene passere gjennom kompileringstidssjekker".

Detaljer

Som du nevnte, fungerer det i JavaScript å bare mocke det som trengs for testen.

I TypeScript vil den samme mocken forårsake en feil siden den ikke helt samsvarer med den forventede typen.

I situasjoner som dette, der du har en mock som du vet ikke samsvarer med forventet type, kan du bruke any for å tillate at mocken går gjennom kompileringstidssjekker.

Her er en oppdatert 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

For Typescript-brukere, en alternativ tilnærming bare for interesse skyld.

For de som ikke liker å bruke noen, her er en løsning som bruker Typescripts Partial-nyttetype og typepåstander (ved å bruke '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}/>);
    });
});

Ved å bruke Partial, nyter vi våre redigerings-/IDE-intellisense-funksjoner, type-erklæringen 'as' brukt i navigasjonsprop-definisjonen kunne erstattes med any, jeg liker bare ikke å bruke any hvis jeg ikke må.

Du kunne bygge videre på grunnprinsippene i denne kodebiten for det spesifikke scenariet som er lagt frem i spørsmålet.

Relaterte artikler