【问题标题】:How to mock React Navigation's navigation prop for unit tests with TypeScript in React Native?如何在 React Native 中使用 TypeScript 模拟 React Navigation 的导航道具以进行单元测试?
【发布时间】:2019-03-05 06:49:48
【问题描述】:

我正在使用 TypeScript 构建一个 React Native 应用程序。对于我的导航,我使用 React Navigation,对于我的单元测试,我使用 Jest 和 Enzyme。

这是我的一个屏幕 (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;

在尝试测试屏幕时,我遇到了一个问题。屏幕需要一个类型为 NavigiationScreenProps 的道具,因为我正在访问 React Navigations navigation 道具。这是测试文件的代码(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);
    });
  });
});

问题是,LoadingScreen 需要一个 navigation 属性。

我得到错误:

[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

我该如何解决这个问题?

我想我不得不以某种方式模拟 navigation 道具。我尝试过这样做(如您所见,我在测试中从 React Navigation 导入了 *),但无法弄清楚。只有NavigationActions 远程有用,但它只包括navigate()。 TypeScript 期望一切,甚至是状态,都会被嘲笑。如何模拟 navigation 道具?

编辑 1: 使用 NavigationScreenProps 的方法是否正确或者我应该使用 interface Props 方法?如果是,那么您将如何模拟(它会导致相同的错误)。

编辑 2: 将第二种方法与接口和

export class LoadingScreen extends Component<Props, object>

我能够“解决”这个问题。我真的不得不像这样模拟导航对象的每个属性:

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

问题仍然存在:这是正确的吗?还是有更好的解决方案?

编辑 3: 当我使用 JS 时,只模拟我需要的属性就足够了(通常只是导航)。但是自从我开始使用 TypeScript 后,我​​不得不模拟导航的每一个方面。否则 TypeScript 会抱怨组件需要不同类型的道具。

【问题讨论】:

    标签: typescript unit-testing react-native jestjs react-navigation


    【解决方案1】:

    对于 Typescript 用户,只是出于兴趣考虑的另一种方法。

    对于那些不喜欢使用任何东西的人,这里有一个利用 Typescript 的 Partial 实用程序类型和类型断言(使用 '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}/>);
        });
    });
    

    使用 Partial,我们享受我们的编辑器/IDE 智能感知功能,导航道具定义中使用的类型断言“as”可以替换为 any,我只是不喜欢使用 any,如果我没有到。

    您可以在此代码 sn-p 的基础上构建问题中列出的特定场景。

    【讨论】:

      【解决方案2】:

      我对我的解决方案不太满意,但我开始只模拟特定组件所需的 Navigation 属性,例如:

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

      在测试中,我可以提供这些作为模拟:

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

      NavigationFunction的定义也是满口的:

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

      我无法为 Navigator 的函数 addListener 和 removeListener 提供正确的签名

      【讨论】:

        【解决方案3】:

        问题

        mock 与预期类型不匹配,因此 TypeScript 报告错误。

        解决方案

        你可以使用any"to opt-out of type-checking and let the values pass through compile-time checks"类型。

        详情

        正如您所提到的,在 JavaScript 中,它只模拟测试所需的内容。

        在 TypeScript 中,相同的 mock 会导致错误,因为它与预期的类型不完全匹配。

        在这种情况下,如果您知道自己的模拟与预期类型不匹配,您可以使用any 允许模拟通过编译时检查。


        这是一个更新的测试:

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

        【讨论】:

        • 不错tutorial,顺便说一句。我用它搭建环境来看这个问题?
        • 谢谢!这很有趣,因为我记得你帮我解决了我遇到的其他一些关键问题。会为你传播你的知识! ?
        • 大声笑,听起来不错。知识并不是真正的“我的”,所以传播它......知识在共享时效果最好?
        • 那么功能组件呢?
        【解决方案4】:

        是的,这就是您模拟 navigation 属性的方式,就像任何其他属性一样。我在我的项目中就是这样做的。

        一个建议:不要模拟每个属性。仅模拟组件中真正需要的那些,然后调用。

        【讨论】:

        • 因为我使用的是 TypeScript,所以这行不通。如果我不模拟每个属性,TypeScript 会抱怨组件需要不同类型的 porp。
        • 抱歉,没有注意到您使用的是TypeScript。我没有使用它,但我真的认为这是正确的等待。您将始终使用方法传递道具。你不能模拟你不会使用的那些,而只是定义一些像这样navigate: () =&gt; {}
        • 没错,但是我更喜欢这种模拟它的方式,因为如果我需要它,我可以将这个模拟用于所有事情。我希望可能有一种方法可以使用 TypeScript 在 Jest 中自动模拟对象,因为 TypeScript 知道对象的属性。也许精通 TypeScript 的人会想出更好的解决方案。无论如何,谢谢您的意见!
        • 对于组件肯定是,您可以使用设置文件为所有测试模拟它们。对于我不相信的组件,使用或不使用 TypeScript。我不知道您是否可以为导航创建一个类型并模拟该类型......如果可能的话,那肯定是一个更好的解决方案。但由于我不使用 TypeScript,我不能说这是正确的,也不能说它会起作用。
        猜你喜欢
        • 1970-01-01
        • 2017-03-06
        • 2020-10-27
        • 1970-01-01
        • 2018-02-01
        • 1970-01-01
        • 2021-02-04
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多