如何在React Native中使用TypeScript模拟React Navigation的导航道具进行单元测试?

时间:2018-09-29 14:44:54

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

我正在用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会抱怨组件期望使用其他类型的道具。

3 个答案:

答案 0 :(得分:8)

问题

该模拟与预期的类型不匹配,因此TypeScript报告错误。

解决方案

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

详细信息

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

在TypeScript中,同一模拟程序会导致错误,因为它与所需类型不完全匹配。

在这种情况下,如果您知道某个模拟不符合预期的类型,则可以使用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
    });
  });
});

答案 1 :(得分:0)

我对我的解决方案不是很满意,但我已经开始只模拟特定组件所需的 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;

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

答案 2 :(得分:-1)

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

一个建议:不要嘲笑每个属性。仅模拟您真正在组件中需要的组件,它将被调用。