繁体   English   中英

React Native Jest - 如何使用多个钩子测试功能组件? 无法存根 AccessiblityInfo 模块

[英]React Native Jest - How to test Functional component with multiple hooks? Unable to stub AccessiblityInfo module

我正在尝试为我最近编写的功能组件编写单元测试。 该组件使用多个钩子,包括useStateuseEffectuseSelector 我发现为所述组件编写测试非常困难,因为我已经读到更改 state 而只是测试结果并不是一个好习惯。

现在我一直在编写非常简单的单元测试,我似乎无法开始工作。 我的第一个测试的目标是存根AccessibilityInfo isScreenReaderEnabled以返回 true,以便我可以验证在我们启用屏幕阅读器时应该出现的组件的存在。 我正在使用sinon来存根AccessibilityInfo ,但是当我挂载我的组件时,我正在寻找的子组件不存在并且测试失败。 我不明白为什么它会失败,因为我认为我已经正确地存根了所有东西,但看起来我做错了什么。

我将在下面添加我的组件和测试文件。 两者都被精简为最相关的代码。

家庭区域组件:

const MAP_MARKER_LIMIT = 3;
const MAP_DELTA = 0.002;
const ACCESSIBILITY_MAP_DELTA = 0.0002;

type HomeAreaProps = {
  onDismiss: () => void;
  onBack: () => void;
  onCompleted: (region: Region) => void;
  getHomeFence: (deviceId: string) => void;
  setHomeFence: (deviceId: string, location: LatLng) => void;
  initialRegion: LatLng | undefined;
  deviceId: string;
};

const HomeArea = (props: HomeAreaProps) => {
  // reference to map view
  const mapRef = useRef<MapView | null>(null);

  // current app state
  let previousAppState = useRef(RNAppState.currentState).current;

  const initialRegion = {
    latitude: parseFloat((props.initialRegion?.latitude ?? 0).toFixed(6)),
    longitude: parseFloat((props.initialRegion?.longitude ?? 0).toFixed(6)),
    latitudeDelta: MAP_DELTA,
    longitudeDelta: MAP_DELTA,
  };

  // modified region of senior
  const [region, setRegion] = useState(initialRegion);

  // is accessibility screen reader enabled
  const [isScreenReaderEnabled, setIsScreenReaderEnabled] = useState(false);

  // state for floating modal
  const [showFloatingModal, setShowFloatingModal] = useState(false);

  // state for center the zone alert screen
  const [showAlertScreen, setShowAlertScreen] = useState(false);

  // state for center the zone error screen
  const [showErrorScreen, setShowErrorScreen] = useState(false);

  // To query error status after a request is made, default to false incase 
  // error cannot be queried from store
  const requestError = useSelector<AppState, boolean>((state) => {
    if (state.homeFence[props.deviceId]) {
      return state.homeZoneFence[props.deviceId].error;
    } else {
      return false;
    }
  });

  // To access device data from redux store, same as above if device data 
  // can't be queried then set to null
  const deviceData = useSelector<AppState, HomeDeviceData | null | undefined>(
    (state) => {
      if (state.homeFence[props.deviceId]) {
        return state.homeFence[props.deviceId].deviceData;
      } else {
        return null;
      }
    }
  );
  const [initialHomeData] = useState<HomeDeviceData | null | undefined>(
    deviceData
  );

  // didTap on [x] button
  const onDismiss = () => {
    setShowFloatingModal(true);
  };

  // didTap on 'save' button
  const onSave = () => {
    if (
      didHomeLocationMovePastLimit(
        region.latitude,
        region.longitude,
        MAP_MARKER_LIMIT
      )
    ) {
      setShowAlertScreen(true);
    } else {
      updateHomeFence();
    }
  };

  const onDismissFloatingModal = () => {
    setShowFloatingModal(false);
    props.getHomeFence(props.deviceId);
    props.onDismiss();
  };

  const onSaveFloatingModal = () => {
    setShowFloatingModal(false);

    if (
      didHomeLocationMovePastLimit(
        region.latitude,
        region.longitude,
        MAP_MARKER_LIMIT
      )
    ) {
      setShowFloatingModal(false);
      setShowAlertScreen(true);
    } else {
      updateHomeFence();
    }
  };

  const onDismissModal = () => {
    setShowFloatingModal(false);
  };

  // Center the Zone Alert Screen
  const onBackAlert = () => {
    // Go back to center the zone screen
    setShowAlertScreen(false);
  };

  const onNextAlert = () => {
    updateHomeFence();
    setShowAlertScreen(false);
  };

  // Center the Zone Error Screen
  const onBackError = () => {
    setShowErrorScreen(false);
  };

  const onNextError = () => {
    updateHomeFence();
  };

  const didHomeLocationMovePastLimit = (
    lat: number,
    lon: number,
    limit: number
  ) => {
    if (
      lat !== undefined &&
      lat !== null &&
      lon !== undefined &&
      lon !== null
    ) {
      const haversineDistance = haversineFormula(
        lat,
        lon,
        initialRegion.latitude,
        initialRegion.longitude,
        "M"
      );
      return haversineDistance > limit;
    }
    return false;
  };

  // didTap on 'reset' button
  const onReset = () => {
    // animate to initial region
    if (initialRegion && mapRef) {
      mapRef.current?.animateToRegion(initialRegion, 1000);
    }
  };

  // did update region by manually moving map
  const onRegionChange = (region: Region) => {
    setRegion({
      ...initialRegion,
      latitude: parseFloat(region.latitude.toFixed(6)),
      longitude: parseFloat(region.longitude.toFixed(6)),
    });
  };

  // didTap 'left' map control
  const onLeft = () => {
    let adjustedRegion: Region = {
      ...region,
      longitude: region.longitude - ACCESSIBILITY_MAP_DELTA,
    };
    // animate to adjusted region
    if (mapRef) {
      mapRef.current?.animateToRegion(adjustedRegion, 1000);
    }
  };

  // didTap 'right' map control
  const onRight = () => {
    let adjustedRegion: Region = {
      ...region,
      longitude: region.longitude + ACCESSIBILITY_MAP_DELTA,
    };
    // animate to adjusted region
    if (mapRef) {
      mapRef.current?.animateToRegion(adjustedRegion, 1000);
    }
  };

  // didTap 'up' map control
  const onUp = () => {
    let adjustedRegion: Region = {
      ...region,
      latitude: region.latitude + ACCESSIBILITY_MAP_DELTA,
    };
    // animate to adjusted region
    if (mapRef) {
      mapRef.current?.animateToRegion(adjustedRegion, 1000);
    }
  };

  // didTap 'down' map control
  const onDown = () => {
    let adjustedRegion: Region = {
      ...region,
      latitude: region.latitude - ACCESSIBILITY_MAP_DELTA,
    };
    // animate to adjusted region
    if (mapRef) {
      mapRef.current?.animateToRegion(adjustedRegion, 1000);
    }
  };

  const updateHomeFence = () => {
    const lat = region.latitude;
    const lon = region.longitude;

    const location: LatLng = {
      latitude: lat,
      longitude: lon,
    };
    props.setHomeFence(props.deviceId, location);
  };

  // gets accessibility status info
  const getAccessibilityStatus = () => {
    AccessibilityInfo.isScreenReaderEnabled()
      .then((isEnabled) => setIsScreenReaderEnabled(isEnabled))
      .catch((error) => console.log(error));
  };

  // listener for when the app changes app state
  const onAppStateChange = (nextAppState: AppStateStatus) => {
    if (nextAppState === "active" && previousAppState === "background") {
      // when we come to the foreground from the background we should 
      // check the accessibility status again
      getAccessibilityStatus();
    }
    previousAppState = nextAppState;
  };

  useEffect(() => {
    getAccessibilityStatus();

    RNAppState.addEventListener("change", onAppStateChange);

    return () => RNAppState.removeEventListener("change", onAppStateChange);
  }, []);

  useEffect(() => {
    // exit screen if real update has occurred, i.e. data changed on backend
    // AND if there is no request error
    if (initialHomeData !== deviceData && initialHomeData && deviceData) {
      if (!requestError) {
        props.onCompleted(region);
      }
    }
    setShowErrorScreen(requestError);
  }, [requestError, deviceData]);

  return (
    <DualPane>
      <TopPane>
        <View style={styles.mapContainer}>
          <MapView
            accessible={false}
            importantForAccessibility={"no-hide-descendants"}
            style={styles.mapView}
            provider={PROVIDER_GOOGLE}
            showsUserLocation={false}
            zoomControlEnabled={!isScreenReaderEnabled}
            pitchEnabled={false}
            zoomEnabled={!isScreenReaderEnabled}
            scrollEnabled={!isScreenReaderEnabled}
            rotateEnabled={!isScreenReaderEnabled}
            showsPointsOfInterest={false}
            initialRegion={initialRegion}
            ref={mapRef}
            onRegionChange={onRegionChange}
          />
          <ScrollingHand />
          {isScreenReaderEnabled && (
            <MapControls
              onLeft={onLeft}
              onRight={onRight}
              onUp={onUp}
              onDown={onDown}
            />
          )}
          {region && <PulsingMarker />}
          {JSON.stringify(region) !== JSON.stringify(initialRegion) && (
            <Button
              style={[btn, overrideButtonStyle]}
              label={i18n.t("homeZone.homeZoneArea.buttonTitle.reset")}
              icon={reset}
              onTap={onReset}
              accessibilityLabel={i18n.t(
                "homeZone.homeZoneArea.buttonTitle.reset"
              )}
            />
          )}
        </View>
      </TopPane>
      <OneButtonBottomPane
        onPress={onSave}
        buttonLabel={i18n.t("homeZone.homeZoneArea.buttonTitle.save")}
      >
        <View style={styles.bottomPaneContainer}>
          <BottomPaneText
            title={i18n.t("homeZone.homeZoneArea.title")}
            content={i18n.t("homeZone.homeZoneArea.description")}
          />
        </View>
      </OneButtonBottomPane>
      <TouchableOpacity
        style={styles.closeIconContainer}
        onPress={onDismiss}
        accessibilityLabel={i18n.t("homeZone.homeZoneArea.buttonTitle.close")}
        accessibilityRole={"button"}
      >
        <Image
          style={styles.cancelIcon}
          source={require("../../../assets/home-zone/close.png")}
        />
      </TouchableOpacity>
      <HomeFloatingModal
        showFloatingModal={showFloatingModal}
        onDismiss={onDismissModal}
        onDiscard={onDismissFloatingModal}
        onSave={onSaveFloatingModal}
      />
      <HomeAlert
        isVisible={showAlertScreen}
        modalTitle={i18n.t("home.feedbackCenter.title.confirmZoneCenter")}
        modalDescription={i18n.t(
          "home.feedbackCenter.description.confirmZoneCenter"
        )}
        onBackButtonTitle={i18n.t("home.feedback.buttonTitle.back")}
        onNextButtonTitle={i18n.t("home.feedback.buttonTitle.okay")}
        onBack={onBackAlert}
        onNext={onNextAlert}
      />
      <HomeAlert
        isVisible={showErrorScreen}
        sentimentType={SentimentType.alert}
        showWarningIcon={false}
        modalTitle={i18n.t("home.errorScreen.title")}
        modalDescription={i18n.t("home.errorScreen.description")}
        onBackButtonTitle={i18n.t("home.errorScreen.buttonTitle.cancel")}
        onNextButtonTitle={i18n.t("home.errorScreen.buttonTitle.tryAgain")}
        onBack={onBackError}
        onNext={onNextError}
      />
    </DualPane>
  );
};

export default HomeArea;

家庭区域测试:

import "jsdom-global/register";
import React from "react";
import { AccessibilityInfo } from "react-native";
import HomeArea from "../../../src/home/components/home-area";
import HomeAlert from "../../../src/home/components/home-alert";
import MapControls from "../../../src/home/components/map-controls";
import { mount } from "enzyme";
import { Provider } from "react-redux";
import configureStore from "redux-mock-store";
import sinon from "sinon";

jest.useFakeTimers();

const mockStore = configureStore();
const initialState = {
  homeFence: {
    "c9035f03-b562-4670-86c6-748b56f02aef": {
      deviceData: {
        eTag: "964665368A4BD68CF86B525385BA507A3D7F5335",
        fences: [
          {
            pointsOfInterest: [
              {
                latitude: 32.8463898,
                longitude: -117.2776381,
                radius: 100,
                uncertainty: 0,
                poiSource: 2,
              },
            ],
            id: "5e1e0bc0-880d-4b0c-a0fa-268975f3046b",
            timeZoneId: "America/Los_Angeles",
            type: 7,
            name: "Children's Pool",
          },
          {
            pointsOfInterest: [
              {
                latitude: 32.9148887,
                longitude: -117.228307,
                radius: 100,
                uncertainty: 0,
                poiSource: 2,
              },
            ],
            id: "782d8fcd-242d-47c0-872b-f669e7ca81c7",
            timeZoneId: "America/Los_Angeles",
            type: 1,
            name: "Home",
          },
        ],
      },
      error: false,
    },
  },
};
const initialStateWithError = {
  homeFence: {
    "c9035f03-b562-4670-86c6-748b56f02aef": {
      deviceData: {
        eTag: "964665368A4BD68CF86B525385BA507A3D7F5335",
        fences: [],
      },
      error: true,
    },
  },
};
const store = mockStore(initialState);

const props = {
  onDismiss: jest.fn(),
  onBack: jest.fn(),
  onCompleted: jest.fn(),
  getHomeZoneFence: jest.fn(),
  setHomeZoneFence: jest.fn(),
  initialRegion: { latitude: 47.6299, longitude: -122.3537 },
  deviceId: "c9035f03-b562-4670-86c6-748b56f02aef",
};

// https://github.com/react-native-maps/react-native-maps/issues/2918#issuecomment-510795210
jest.mock("react-native-maps", () => {
  const { View } = require("react-native");
  const MockMapView = (props: any) => {
    return <View>{props.children}</View>;
  };
  const MockMarker = (props: any) => {
    return <View>{props.children}</View>;
  };
  return {
    __esModule: true,
    default: MockMapView,
    Marker: MockMarker,
  };
});

describe("<HomeArea />", () => {
  describe("accessibility", () => {
    it("should return true and we should have map control present", async () => {
      sinon.stub(AccessibilityInfo, "isScreenReaderEnabled").callsFake(() => {
        return new Promise((res, _) => {
          res(true);
        });
      });
      const wrapper = mount(
        <Provider store={store}>
          <HomeArea {...props} />
        </Provider>
      );
      expect(wrapper).not.toBeUndefined(){jest.fn()} onRight={jest.fn()} onUp={jest.fn()} onDown={jest.fn()} />).instance()).not.toBeUndefined();

      expect(wrapper.find(MapControls).length).toEqual(1);
    });
  });

  describe("requestError modal", () => {
    it("should render requestErrorModal", async () => {
      const store = mockStore(initialStateWithError);
      const wrapper = mount(
        <Provider store={store}>
          <HomeArea {...props} />
        </Provider>
      );
      expect(wrapper).not.toBeUndefined();

      expect(
        wrapper.contains(
          <HomeAlert
            isVisible={false}
            modalTitle={""}
            modalDescription={""}
            onBackButtonTitle={""}
            onNextButtonTitle={""}
            onBack={jest.fn()}
            onNext={jest.fn()}
          />
        )
      ).toBe(true);
    });
  });
});

我的一个想法是在我的组件中存根getAccessibilityStatus但没有任何运气这样做。 我一直在阅读在线功能组件有点“黑匣子”并且存根功能似乎不可能,这是真的吗? 我开始想知道如果多个钩子以及它是一个功能组件的事实使得这样做非常困难,我如何能够成功地测试我的组件。

任何帮助是极大的赞赏。

这可能是因为在您检查组件是否存在之前,promise 没有解析。 你可以在这里阅读更多关于它https://www.benmvp.com/blog/asynchronous-testing-with-enzyme-react-jest/

像这样试试

const runAllPromises = () => new Promise(setImmediate)
...
  describe("accessibility", () => {
    it("should return true and we should have map control present", async () => {
      sinon.stub(AccessibilityInfo, "isScreenReaderEnabled").callsFake(() => {
        return new Promise((res, _) => {
          res(true);
        });
      });
      const wrapper = mount(
        <Provider store={store}>
          <HomeArea {...props} />
        </Provider>
      );
      await runAllPromises()
      // after waiting for all the promises to be exhausted
      // we can do our UI check
      component.update()
      expect(wrapper).not.toBeUndefined();
      expect(wrapper.find(MapControls).length).toEqual(1);
    });
  });
...

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM