React Native 上的 Google 地图、地理定位和单元测试

2025-06-08

React Native 上的 Google 地图、地理定位和单元测试

在本教程中,我们将使用 React Native CLI 构建一个适用于 iOS 和 Android 的出租车应用。此外,我们还将深入使用 Jest + React 测试库对整个应用进行单元测试。

源代码

我还有一个视频版本,这是本教程的专业功能。快来看看吧🚨🚀👉视频版本

环境设置

首先,请确保在开始之前已准备好开发环境。我使用的是一台 macOS 笔记本电脑,并配备了两部 iPhone:iPhone 12 和 iPhone SE 2020。虽然测试 App 并不需要真机,但如果没有 Android/iPhone,也可以使用模拟器。不过,我们建议您在真机上测试 App。

我使用的是 React Native 0.64 版本,请确保你也使用相同的版本,以避免在使用 react-native-maps 等主要库时出现兼容性问题。我会尽量使本教程与 React Native CLI 的主要版本保持同步。

点击此链接安装本地环境。开始吧!

创建应用程序

让我们使用 npx 命令创建应用程序

npx react-native init taxiApp --version 0.64.2
Enter fullscreen mode Exit fullscreen mode

创建一个src文件夹,并将 App.js 文件移动到该位置。最终,你应该得到./src/App.js。我们这个项目不使用 TypeScript (TS),因此请删除所有与 TS 相关的代码,并将 TypeScript App 函数转换为常规 JavaScript 函数。

import React from "react"
import { SafeAreaView, StatusBar, StyleSheet, Text, View } from "react-native"

const App = () => {
  return (
    <SafeAreaView>
      <StatusBar barStyle="dark-content" />
      <View style={styles.sectionContainer}>
        <Text style={styles.sectionTitle}>Welcome to Taxi App</Text>
      </View>
    </SafeAreaView>
  )
}

const styles = StyleSheet.create({
  sectionContainer: {
    marginTop: 32,
    paddingHorizontal: 24,
  },
  sectionTitle: {
    fontSize: 24,
    fontWeight: "600",
  },
  sectionDescription: {
    marginTop: 8,
    fontSize: 18,
    fontWeight: "400",
  },
})

export default App
Enter fullscreen mode Exit fullscreen mode

我已经使用来自 React Native 的StyleSheet对象放置了一个临时的“欢迎使用出租车应用程序”消息,其中包含一些样式,这很好,但这只是暂时的,因为我们很快就会进入样式组件

因为我将App.js移到了src文件夹中,所以我们必须更新App.js以在主index.js中导入新位置


import App from "./App"
Enter fullscreen mode Exit fullscreen mode


import App from "./src/App"
Enter fullscreen mode Exit fullscreen mode

在模拟器中运行

首先我们需要启动 Metro。
要启动 Metro,请在 React Native 项目文件夹中运行npx react-native start :

npx react-native start
Enter fullscreen mode Exit fullscreen mode

让 Metro Bundler 在它自己的终端中运行。在 React Native 项目文件夹中打开一个新终端。运行以下命令:

npx react-native run-ios
Enter fullscreen mode Exit fullscreen mode

如果一切设置正确,您应该很快就会看到您的新应用程序在您的 iPhone 模拟器中运行。

对于 Android,首先运行 Android 模拟器,然后运行以下命令:

npx react-native run-android
Enter fullscreen mode Exit fullscreen mode

安装 React Native Maps

React Native 中用于处理 Maps 的默认库是react-native-maps,接下来我们将使用它。

npm install react-native-maps --save-exact
Enter fullscreen mode Exit fullscreen mode

我们将使用 Google 地图而不是 Apple 地图,因为 Apple 地图仅适用于 iOS 设备。要继续,我们需要Android SDKiOS SDK的 API 密钥。您需要创建一个Google 结算账户,因此请点击链接并创建一个账户。

Google 结算帐户

创建 Google Cloud 帐户后,您需要创建一个新项目。在新项目中,我们将创建用于访问Android 版 Maps SDKiOS 版 Maps SDK等的API 和服务。

  1. 在console.cloud.google.com内创建新项目
  2. 单击 API 和服务 > 凭证。
  3. 点击“+ 创建凭据”。它会立即生成一个 API 密钥。我们将用这个密钥在 iOS 和 Android 设备上设置 Google 地图。
  4. 点击最近创建的凭据来限制密钥。搜索“API 限制”部分。您将看到一个“限制密钥”选项。请确保选择“Maps SDK for Android”“Maps SDK for iOS”
  5. 点击“保存”

Google 结算帐户

在 iOS 上构建配置

设置使用描述属性

应用程序的Info.plist文件必须包含一个NSLocationWhenInUseUsageDescription,其中包含面向用户的目的字符串,清楚完整地解释您的应用程序为什么需要该位置,否则 Apple 将拒绝您的应用程序提交。

在你的./ios/taxiApp/Info.plist,确保你有这个:

...
<key>NSLocationWhenInUseUsageDescription</key>
<string>In order to work we need you to grant location access</string>
...
Enter fullscreen mode Exit fullscreen mode

在 iOS 中启用 Google 地图

如果您想在 iOS 上启用 Google 地图,请复制 Google API 密钥并按如下方式编辑您的./ios/taxiApp/AppDelegate.m :

+ #import <GoogleMaps/GoogleMaps.h>

@implementation AppDelegate
...

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
+  [GMSServices provideAPIKey:@"_YOUR_API_KEY_"]; // add this line using the api key obtained from Google Console
...
Enter fullscreen mode Exit fullscreen mode

[GMSServices provideAPIKey] 应该是该方法的第一次调用。

将以下内容添加到Podfile中的config = use_native_modules!上方

# React Native Maps dependencies
rn_maps_path = '../node_modules/react-native-maps'
pod 'react-native-google-maps', :path => rn_maps_path
pod 'GoogleMaps'
pod 'Google-Maps-iOS-Utils'
Enter fullscreen mode Exit fullscreen mode

现在,我们将使用 CocoaPods 为 iOS 构建应用。安装 npm 包后,我们需要安装 pod。

npx pod-install
Enter fullscreen mode Exit fullscreen mode

在 Android 上构建配置

配置 Google Play 服务。在android/build.gradle中添加以下两行:

ext {
        buildToolsVersion = "29.0.3"
        minSdkVersion = 21
        compileSdkVersion = 29
        targetSdkVersion = 29
        ndkVersion = "20.1.5948944"
        playServicesVersion = "17.0.0"    // <= 👈
        androidMapsUtilsVersion = "2.2.0" // <= 👈
    }
Enter fullscreen mode Exit fullscreen mode

指定你的 Google 地图 API 密钥。将你的 API 密钥添加到清单文件 (android/app/src/main/AndroidManifest.xml) 中:

<application>
   <!-- You will only need to add this meta-data tag, but make sure it's a child of application -->
   <meta-data
     android:name="com.google.android.geo.API_KEY"
     android:value="Your Google maps API Key Here"/>

   <!-- You will also only need to add this uses-library tag -->
   <uses-library android:name="org.apache.http.legacy" android:required="false"/>
</application>
Enter fullscreen mode Exit fullscreen mode

开始使用 React Native Maps

打开 App.js 文件,并将内容替换为以下代码。目的是让 Google 地图覆盖设备的整个屏幕。为此,我们添加了新的样式:

import React from "react"
import { SafeAreaView, StatusBar, StyleSheet } from "react-native"
import MapView, { PROVIDER_GOOGLE } from "react-native-maps"

const App = () => {
  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle="dark-content" />
      <MapView
        style={styles.map}
        provider={PROVIDER_GOOGLE}
        initialRegion={{
          latitude: 57.709127,
          longitude: 11.934746,
          latitudeDelta: 0.0922,
          longitudeDelta: 0.0421,
        }}
      />
    </SafeAreaView>
  )
}

const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    alignItems: "center",
  },
  map: {
    ...StyleSheet.absoluteFillObject,
  },
})

export default App
Enter fullscreen mode Exit fullscreen mode

如果您在模拟器上运行该应用程序,您应该会看到类似这样的内容:

谷歌地图封面

显示用户位置

我们将请求用户授予位置权限,以便在地图上显示用户的位置。为此,我们将使用react-native-permissions包。您可以按照此处的说明,了解如何在 iOS 和 Android 上进行设置

npm install --save-exact react-native-permissions@3.0.1
Enter fullscreen mode Exit fullscreen mode

对于 iOS 设置,您必须打开ios 文件夹内的Podfile并添加下一行代码:

# React Native Permissions
permissions_path = '../node_modules/react-native-permissions/ios'
pod 'Permission-LocationWhenInUse', :path => "#{permissions_path}/LocationWhenInUse"
Enter fullscreen mode Exit fullscreen mode

之前我们已经在 Info.plist 中添加了以下几行,但您可以仔细检查一下:

<key>NSLocationWhenInUseUsageDescription</key>
<string>In order to work we need you to grant location access</string>
Enter fullscreen mode Exit fullscreen mode

现在使用 Cocoa Pods 安装依赖项以完成 ios 的进程。

npx pod-install
Enter fullscreen mode Exit fullscreen mode

对于 Android,您只需更新android/app/src/main/AndroidManifest.xml文件。

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
Enter fullscreen mode Exit fullscreen mode

现在,让我们使用 React Native 权限来显示用户位置。在 App.js 文件中添加以下几行:

import React, { useEffect } from "react"
import { SafeAreaView, StatusBar, StyleSheet } from "react-native"
import MapView, { PROVIDER_GOOGLE } from "react-native-maps"
import { check, request, PERMISSIONS, RESULTS } from "react-native-permissions" // 👈

const App = () => {
  const handleLocationPermission = async () => { // 👈
    let permissionCheck = '';
    if (Platform.OS === 'ios') {
      permissionCheck = await check(PERMISSIONS.IOS.LOCATION_WHEN_IN_USE);

      if (
        permissionCheck === RESULTS.BLOCKED ||
        permissionCheck === RESULTS.DENIED
      ) {
        const permissionRequest = await request(
          PERMISSIONS.IOS.LOCATION_WHEN_IN_USE,
        );
        permissionRequest === RESULTS.GRANTED
          ? console.warn('Location permission granted.')
          : console.warn('location permission denied.');
      }
    }

    if (Platform.OS === 'android') {
      permissionCheck = await check(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION);

      if (
        permissionCheck === RESULTS.BLOCKED ||
        permissionCheck === RESULTS.DENIED
      ) {
        const permissionRequest = await request(
          PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
        );
        permissionRequest === RESULTS.GRANTED
          ? console.warn('Location permission granted.')
          : console.warn('location permission denied.');
      }
    }
  };

  useEffect(() => {
    handleLocationPermission()
  }, [])

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle="dark-content" />
      <MapView
        style={styles.map}
        provider={PROVIDER_GOOGLE}
        initialRegion={{
          latitude: 57.709127,
          longitude: 11.934746,
          latitudeDelta: 0.0922,
          longitudeDelta: 0.0421,
        }}
        showsUserLocation={true} // 👈
      />
    </SafeAreaView>
  )
}

const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    alignItems: "center",
  },
  map: {
    ...StyleSheet.absoluteFillObject,
  },
})

export default App
Enter fullscreen mode Exit fullscreen mode

如果您使用 iOS 模拟器运行该应用程序,您将看到一个弹出窗口,要求您批准权限。

授予用户位置权限

确认后,您应该会看到一个蓝色圆圈,指示您当前的位置。如果没有,可能是因为您的模拟器没有使用自定义位置,您需要更新它。为此,请进入模拟器菜单并执行以下操作:

  1. 点击功能>位置>自定义位置
  2. 输入此位置:纬度:57,705871 & 经度:11,938823

这个位置是基于地图initialRegion的,在上面的代码中它:

initialRegion={{
    latitude: 57.709127,
    longitude: 11.934746,
    latitudeDelta: 0.0922,
    longitudeDelta: 0.0421,
}}
Enter fullscreen mode Exit fullscreen mode

您可以看到我的自定义位置位于我的初始地图区域内,即瑞典哥德堡市。

追踪用户位置

之前我们只显示用户的当前位置,但当用户改变位置时,我们需要持续跟踪用户位置。为此,我们将使用react-native-geolocation-service

npm install react-native-geolocation-service@5.2.0 --save-exact
Enter fullscreen mode Exit fullscreen mode

设置

您可以参考他们文档中更详细的指南。我们已经为 iOS 和 Android 设置了位置权限。对于 iOS,我们将运行:

npx pod-install
Enter fullscreen mode Exit fullscreen mode

让我们更新我们的 App.js 组件以使用地理位置跟踪用户位置。

import React, { useEffect, useState } from "react"
import { SafeAreaView, StatusBar, StyleSheet } from "react-native"
import MapView, { PROVIDER_GOOGLE } from "react-native-maps"
import { check, request, PERMISSIONS, RESULTS } from "react-native-permissions"
import Geolocation from "react-native-geolocation-service" // 👈

const App = () => {
  const [location, setLocation] = useState(null) // 👈

  const handleLocationPermission = async () => {
    let permissionCheck = ""
    if (Platform.OS === "ios") {
      permissionCheck = await check(PERMISSIONS.IOS.LOCATION_WHEN_IN_USE)

      if (permissionCheck === RESULTS.DENIED) {
        const permissionRequest = await request(
          PERMISSIONS.IOS.LOCATION_WHEN_IN_USE
        )
        permissionRequest === RESULTS.GRANTED
          ? console.warn("Location permission granted.")
          : console.warn("Location perrmission denied.")
      }
    }

    if (Platform.OS === "android") {
      permissionCheck = await check(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION)

      if (permissionCheck === RESULTS.DENIED) {
        const permissionRequest = await request(
          PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION
        )
        permissionRequest === RESULTS.GRANTED
          ? console.warn("Location permission granted.")
          : console.warn("Location perrmission denied.")
      }
    }
  }

  useEffect(() => {
    handleLocationPermission()
  }, [])

  useEffect(() => { // 👈
    Geolocation.getCurrentPosition(
      position => {
        const { latitude, longitude } = position.coords
        setLocation({ latitude, longitude })
      },
      error => {
        console.log(error.code, error.message)
      },
      { enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 }
    )
  }, [])

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle="dark-content" />
      {location && ( // 👈
        <MapView
          style={styles.map}
          provider={PROVIDER_GOOGLE}
          initialRegion={{
            latitude: location.latitude,  // 👈
            longitude: location.longitude,// 👈
            latitudeDelta: 0.0922,
            longitudeDelta: 0.0421,
          }}
          showsUserLocation={true}
        />
      )}
    </SafeAreaView>
  )
}

const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    alignItems: "center",
  },
  map: {
    ...StyleSheet.absoluteFillObject,
  },
})

export default App
Enter fullscreen mode Exit fullscreen mode

继续。

  1. 使用 useState 添加本地状态const [location, setLocation] = useState(null)
  2. 当组件安装时使用 useEffect 我们调用地理位置服务并更新位置状态。
  3. <MapView />添加了一个条件,仅当位置状态不为空时才显示组件

向 MapView 添加自定义样式和属性

我们可以更改 Google 地图的颜色和整体外观。此外,MapView 组件附带一些有用的属性,我们将添加其中几个,但您可以在此处找到完整列表。

让我们在 src 文件夹中创建一个名为 style 的新文件夹;这将是我们将添加更多内容的通用样式位置
./src/styles/index.js

export const customStyleMap = [
  {
    elementType: "geometry",
    stylers: [
      {
        color: "#242f3e",
      },
    ],
  },
  {
    elementType: "labels.text.fill",
    stylers: [
      {
        color: "#746855",
      },
    ],
  },
  {
    elementType: "labels.text.stroke",
    stylers: [
      {
        color: "#242f3e",
      },
    ],
  },
  {
    featureType: "administrative.locality",
    elementType: "labels.text.fill",
    stylers: [
      {
        color: "#d59563",
      },
    ],
  },
  {
    featureType: "poi",
    elementType: "labels.text.fill",
    stylers: [
      {
        color: "#d59563",
      },
    ],
  },
  {
    featureType: "poi.park",
    elementType: "geometry",
    stylers: [
      {
        color: "#263c3f",
      },
    ],
  },
  {
    featureType: "poi.park",
    elementType: "labels.text.fill",
    stylers: [
      {
        color: "#6b9a76",
      },
    ],
  },
  {
    featureType: "road",
    elementType: "geometry",
    stylers: [
      {
        color: "#38414e",
      },
    ],
  },
  {
    featureType: "road",
    elementType: "geometry.stroke",
    stylers: [
      {
        color: "#212a37",
      },
    ],
  },
  {
    featureType: "road",
    elementType: "labels.text.fill",
    stylers: [
      {
        color: "#9ca5b3",
      },
    ],
  },
  {
    featureType: "road.highway",
    elementType: "geometry",
    stylers: [
      {
        color: "#746855",
      },
    ],
  },
  {
    featureType: "road.highway",
    elementType: "geometry.stroke",
    stylers: [
      {
        color: "#1f2835",
      },
    ],
  },
  {
    featureType: "road.highway",
    elementType: "labels.text.fill",
    stylers: [
      {
        color: "#f3d19c",
      },
    ],
  },
  {
    featureType: "transit",
    elementType: "geometry",
    stylers: [
      {
        color: "#2f3948",
      },
    ],
  },
  {
    featureType: "transit.station",
    elementType: "labels.text.fill",
    stylers: [
      {
        color: "#d59563",
      },
    ],
  },
  {
    featureType: "water",
    elementType: "geometry",
    stylers: [
      {
        color: "#17263c",
      },
    ],
  },
  {
    featureType: "water",
    elementType: "labels.text.fill",
    stylers: [
      {
        color: "#515c6d",
      },
    ],
  },
  {
    featureType: "water",
    elementType: "labels.text.stroke",
    stylers: [
      {
        color: "#17263c",
      },
    ],
  },
]
Enter fullscreen mode Exit fullscreen mode

现在,我们在 src 目录下创建更多文件夹。接下来是 screens 文件夹,我们将在其中创建我们的第一个屏幕文件,名为 UserScreen.js。在 UserScreen 文件夹中,我们将移动 App.js 的内容。( ./src/screens/UserScreen.js )

/**
 1. Copy and paste code from App.js
 2. Rename component name from App to UserScreen
 */

import React, { useEffect, useState } from "react"
import { SafeAreaView, StatusBar, StyleSheet } from "react-native"
import MapView, { PROVIDER_GOOGLE } from "react-native-maps"
import { check, request, PERMISSIONS, RESULTS } from "react-native-permissions"
import Geolocation from "react-native-geolocation-service"

const UserScreen = () => {
  const [location, setLocation] = useState(null)

  const handleLocationPermission = async () => {
    let permissionCheck = ""
    if (Platform.OS === "ios") {
      permissionCheck = await check(PERMISSIONS.IOS.LOCATION_WHEN_IN_USE)

      if (permissionCheck === RESULTS.DENIED) {
        const permissionRequest = await request(
          PERMISSIONS.IOS.LOCATION_WHEN_IN_USE
        )
        permissionRequest === RESULTS.GRANTED
          ? console.warn("Location permission granted.")
          : console.warn("Location perrmission denied.")
      }
    }

    if (Platform.OS === "android") {
      permissionCheck = await check(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION)

      if (permissionCheck === RESULTS.DENIED) {
        const permissionRequest = await request(
          PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION
        )
        permissionRequest === RESULTS.GRANTED
          ? console.warn("Location permission granted.")
          : console.warn("Location perrmission denied.")
      }
    }
  }

  useEffect(() => {
    handleLocationPermission()
  }, [])

  useEffect(() => {
    Geolocation.getCurrentPosition(
      position => {
        const { latitude, longitude } = position.coords
        setLocation({ latitude, longitude })
      },
      error => {
        console.log(error.code, error.message)
      },
      { enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 }
    )
  }, [])

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle="dark-content" />
      {location && (
        <MapView
          style={styles.map}
          provider={PROVIDER_GOOGLE}
          initialRegion={{
            latitude: location.latitude,
            longitude: location.longitude,
            latitudeDelta: 0.0922,
            longitudeDelta: 0.0421,
          }}
          showsUserLocation={true}
        />
      )}
    </SafeAreaView>
  )
}

const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    alignItems: "center",
  },
  map: {
    ...StyleSheet.absoluteFillObject,
  },
})

export default UserScreen
Enter fullscreen mode Exit fullscreen mode

之后,我们的 App.js 组件将不再包含上述代码。相反,我们将导入该<UserScreen />组件并进行渲染。我们将在 App.js 中使用 React Navigation 来处理应用程序的所有屏幕。

/**
  For now just import and render <UserScreen />
 */

import React from "react"
import UserScreen from "./screens/UserScreen"

const App = () => {
  return <UserScreen />
}

export default App
Enter fullscreen mode Exit fullscreen mode

我们的文件夹目录应该是这样的。

文件夹组织

最后,让我们将我们的 customMapStyle 和其他道具运用到 UserScreen.js 上的 MapView 中

...
import {customStyleMap} from '../styles'; // 👈

const UserScreen = () => {
  ...

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle="dark-content" />
      {location && (
        <MapView
          ...
          customMapStyle={customStyleMap} // 👈
          paddingAdjustmentBehavior="automatic" // 👈
          showsMyLocationButton={true} // 👈
          showsBuildings={true} // 👈
          maxZoomLevel={17.5} // 👈
          loadingEnabled={true} // 👈
          loadingIndicatorColor="#fcb103" // 👈
          loadingBackgroundColor="#242f3e" // 👈
        />
      )}
    </SafeAreaView>
  );
};

...

export default UserScreen;
Enter fullscreen mode Exit fullscreen mode

我们的地图焕然一新!😍

地图自定义样式

单元测试

测试时间到了!😍

我们将深入研究该<UserScreen />组件的第一个测试用例,我们将使用:

  1. 笑话
  2. React 测试库 Native 📚

因此,让我们安装 React Testing Library Native

npm install --save-dev @testing-library/react-native
Enter fullscreen mode Exit fullscreen mode

另外,让我们安装额外的 jest 匹配器:

npm install --save-dev @testing-library/jest-native
Enter fullscreen mode Exit fullscreen mode

然后使用Jest 配置中的setupFilesAfterEnv选项自动将其添加到您的 jest 测试中(它通常位于 package.json 中的“jest”键下或 jest.config.json 文件中):

// package.json
"jest": {
    "preset": "react-native",
    // 👇
    "setupFilesAfterEnv": [
      "@testing-library/jest-native/extend-expect"
    ]
  }
Enter fullscreen mode Exit fullscreen mode

现在,我们需要用到一些库,例如Geolocationreact-native-permissionsMapView组件,并创建这些组件/库的模拟。为此,我们在应用的根目录中创建一个名为jest-setup.js 的文件。目前,我们只导入 jest。

// ./jest-setup.js
import { jest } from "@jest/globals"
Enter fullscreen mode Exit fullscreen mode

好了,好了,让我们为<UserScreen />组件创建第一个测试。为此,在src/screens中,创建__tests__(双下划线,两侧) 文件夹。在其中创建名为UserScreen.test.js的文件。

// ./src/screens/__tests__/UserScreen.test.js

import React from "react"
import { render, waitFor } from "@testing-library/react-native"
import UserScreen from "../UserScreen"

describe("<UserScreen />", () => {
  test("should renders MapView and Marker with user current location", () => {
    render(<UserScreen />)
  })
})
Enter fullscreen mode Exit fullscreen mode

现在,如果我们尝试运行package.json 文件中已有的测试命令,会发生什么?

npm run test
Enter fullscreen mode Exit fullscreen mode

运行 test 命令后,你会注意到Jest尝试运行我们已有的两个测试文件。该命令将运行我们在应用中定义的所有测试文件。我们默认已经有一个测试,它来自 App.js 文件。第二个测试就是我们上面编写的那个。

另外,你会看到测试失败了!😱

测试失败

测试失败了,这完全正常。您可以看到,问题在于 Jest 尝试从react-native-maps库中为 UserScreen.test.js 文件导入MapView组件,但失败了。这就是为什么我们需要模拟 react-native-maps 以使测试能够通过。

那就行动起来吧!💪

打开jest-setup.js文件并模拟 react-native-maps。

jest.mock("react-native-maps", () => {
  const React = require("react")
  const { View } = require("react-native")
  class MockMapView extends React.Component {
    render() {
      const { testID, children, ...props } = this.props

      return (
        <View
          {...{
            ...props,
            testID,
          }}
        >
          {children}
        </View>
      )
    }
  }

  const mockMapTypes = {
    STANDARD: 0,
    SATELLITE: 1,
    HYBRID: 2,
    TERRAIN: 3,
    NONE: 4,
    MUTEDSTANDARD: 5,
  }

  return {
    __esModule: true,
    default: MockMapView,
    MAP_TYPES: mockMapTypes,
    PROVIDER_DEFAULT: "default",
    PROVIDER_GOOGLE: "google",
  }
})
Enter fullscreen mode Exit fullscreen mode

我们为 react-native-maps 创建了一个模拟组件。我们使用了 React Class 组件,主要是因为我在使用函数组件时遇到了问题。或许你可以尝试用函数组件来代替类组件。我们获取 MapView 组件可能拥有的所有 props,以及作为子组件传递的所有内容。最后,我们返回 MockMapView 作为默认导出,因为当我们从 react-native-maps 导入 MapView 时,你可以看到它是一个默认导出。

接下来,我们需要告诉 Jest 我们有一个用于测试的setupFiles 文件。我们在 package.json 文件的 jest 部分中执行此操作。

"jest": {
    "preset": "react-native",
    "setupFilesAfterEnv": [
      "@testing-library/jest-native/extend-expect"
    ],
    // 👇
    "setupFiles": [
      "./jest-setup.js"
    ]
  }
Enter fullscreen mode Exit fullscreen mode

尝试再次运行测试命令

npm run test
Enter fullscreen mode Exit fullscreen mode

然后...失败了!

第二次失败的测试

这次至少在 MapView 上没有失败。这次失败是因为react-native-permissions。因为我们还没有 mock 这个。

那就行动起来吧!💪

返回jest-setup.js并添加以下内容:

jest.mock("react-native-permissions", () =>
  require("react-native-permissions/mock")
)
Enter fullscreen mode Exit fullscreen mode

如果由于某些原因你仍然遇到问题,export {PERMISSIONS, RESULT}那么你可以尝试将transformIgnorePatterns添加到 package.json 中的 Jest 配置中

 "transformIgnorePatterns": [
      "node_modules/(?!(jest-)?react-native-permissions|)" // See I added react-native-permissions
    ],
Enter fullscreen mode Exit fullscreen mode

我们还缺少最后一个模拟,那就是地理位置。与其在 jest-setup.js 文件中模拟,不如__mocks__在项目根目录创建一个文件夹。在 mocks 文件夹中,添加库的名称react-native-geolocation-service.js。它必须与库的名称相同。

// ./__mocks__/react-native-geolocation-service.js
export default {
  getCurrentPosition: jest.fn().mockImplementation(successCallback => {
    const position = {
      coords: {
        latitude: 57.7,
        longitude: 11.93,
      },
    }
    successCallback(position)
  }),
}
Enter fullscreen mode Exit fullscreen mode

呼,我想我们已经完成了从外部包模拟库/组件的工作。是时候重新运行测试了,但我们可以删除该__tests__/App.test.js文件。我们暂时不测试 App.js。我们专注于src/screens/__tests__/UserScreen.test.js……

npm run test
Enter fullscreen mode Exit fullscreen mode

并且...它应该通过!!

测试通过

我们仅测试了 UserScreen 组件是否渲染。接下来,让我们测试一下 Map 组件是否渲染并调用位置权限来提升代码覆盖率。

// src/screens/__tests__/UserScreen.test.js
import React from "react"
import { render, waitFor } from "@testing-library/react-native"
import UserScreen from "../UserScreen"
// Import check from react-native-permissions
import { check } from "react-native-permissions"
// Import Geolocation also
import Geolocation from "react-native-geolocation-service"

describe("<UserScreen />", () => {
  test("should renders MapView and Marker with user current location", async () => {
    render(<UserScreen />)

    await waitFor(() => {
      expect(check).toHaveBeenCalledTimes(1)
      expect(Geolocation.getCurrentPosition).toHaveBeenCalledTimes(1)
    })
  })
})
Enter fullscreen mode Exit fullscreen mode

我们可以安全地从模拟的 react-native-permissions 库中导入check函数。Geolocation一样。我们使用 React Testing Library Native 中的 async/await 和waitFor,因为组件挂载时,我们首先检查权限。其次,我们调用当前用户位置。然后我们使用 setLocation({latitude, longitude}) 更新状态。所以,有几件事正在进行,我们必须等待这些操作完成。

让我们在 MapView 组件中添加一个 testID,以确保地图能够被渲染。打开 UserScreen 组件并添加一个 testID。

...

const UserScreen = () => {
  ...

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle="dark-content" />
      {location && (
        <MapView
          testID="map" // 👈
          ...
        />
      )}
    </SafeAreaView>
  );
};

...
Enter fullscreen mode Exit fullscreen mode

我们还需要将新的 testID 传递给我们模拟的 MapView 组件。因此,打开 jest-setup.js 文件并添加以下内容:

jest.mock('react-native-maps', () => {
  const React = require('react');
  const {View} = require('react-native');
  class MockMapView extends React.Component {
    render() {
      const {testID, children, ...props} = this.props; // 👈

      return (
        <View
          {...{
            ...props,
            testID, // 👈
          }}>
          {children}
        </View>
      );
    }
  }

...
});
Enter fullscreen mode Exit fullscreen mode

让我们在 UserScreen.test.js 文件中添加最后一个断言。

import React from "react"
import { render, waitFor } from "@testing-library/react-native"
import UserScreen from "../UserScreen"
import { check } from "react-native-permissions"
import Geolocation from "react-native-geolocation-service"

describe("<UserScreen />", () => {
  test("should renders MapView and Marker with user current location", async () => {
    const { getByTestId } = render(<UserScreen />) // 👈

    await waitFor(() => {
      expect(check).toHaveBeenCalledTimes(1)
      expect(Geolocation.getCurrentPosition).toHaveBeenCalledTimes(1)
      expect(getByTestId("map")).toBeDefined() // 👈
    })
  })
})
Enter fullscreen mode Exit fullscreen mode

我们正在使用 React Testing Library Native getByTestId函数来断言 testID 已定义。

🛑 停!

我暂时完成了。请继续关注本教程的后续部分。也欢迎您留下您的评论。

您可以通过cristian.echeverri4@gmail.com邮箱联系我。也可以通过Twitter联系我。

鏂囩珷鏉ユ簮锛�https://dev.to/cecheverri4/google-maps-geolocation-and-unit-test-on-react-native-4eim
PREV
我学习编程第一年犯的五个错误 5 绝对是个错误。有人会说 Stackoverflow 是软件开发史上最好的东西。从网上抄袭解决方案,却不理解其中的含义,根本无法教会你如何编程。有时候,我真希望我们能关掉网络,回到书本上学习。
NEXT
使用 npm 从 Node-Sass 迁移到 Sass(Dart-Sass)