27、定位测试:JUnit单元测试、MockLocationProvider模拟、UI自动化测试

定位功能,可以说是Android应用里最容易出bug的模块之一。为什么?因为真实环境太复杂了——GPS信号时好时坏,网络定位飘忽不定,用户还可能随时关掉定位权限。你想想看,如果每次测试都要跑到户外去走一圈,那开发效率得多低?

所以,这一章我们来聊聊定位测试。我会从三个层面展开:单元测试、模拟定位、UI自动化。说白了,就是让你在办公室里,也能把定位功能测个底朝天。

27.1 JUnit单元测试:把定位逻辑拆开测

我个人习惯,写定位代码之前,先想好怎么测。这不是矫情,而是吃过亏。我曾经在一个地图项目里,把定位解析逻辑和UI混在一起写,结果每次改需求都要重新跑一遍真机,痛苦得很。

单元测试的核心思路,就是把你定位逻辑中的「纯计算」部分抽出来。比如,解析NMEA语句、计算距离、判断是否在某个地理围栏内——这些都不依赖Android系统服务,完全可以用JUnit测。

核心原则:能脱离Android框架的逻辑,就不要依赖Context。

举个例子,假设你有一个工具方法,用来计算两个经纬度之间的距离:

public class LocationUtils {
    public static double calculateDistance(double lat1, double lng1,
                                           double lat2, double lng2) {
        final int R = 6371; // 地球半径,单位公里
        double latDistance = Math.toRadians(lat2 - lat1);
        double lonDistance = Math.toRadians(lng2 - lng1);
        double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2)
                + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
                * Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        return R * c * 1000; // 返回米
    }
}

这个方法的单元测试就很简单:

public class LocationUtilsTest {
    @Test
    public void testCalculateDistance() {
        // 北京天安门
        double lat1 = 39.9042, lng1 = 116.4074;
        // 上海东方明珠
        double lat2 = 31.2304, lng2 = 121.4737;

        double distance = LocationUtils.calculateDistance(lat1, lng1, lat2, lng2);
        // 实际距离约1068公里
        assertEquals(1068000, distance, 50000); // 允许5公里误差
    }
}

嗯,这里要注意:单元测试只测你的代码逻辑,不测系统服务。如果你需要测试LocationManager的调用行为,那就得用Mockito了。

27.2 MockLocationProvider:模拟定位数据

单元测试搞定了纯逻辑,那「定位数据从哪来」这个问题怎么测?这时候就需要MockLocationProvider登场了。

Android系统其实自带了一个模拟定位的机制。你可以在开发者选项里开启「允许模拟位置」,然后通过代码注入假的位置数据。但说实话,手动操作太麻烦。我建议你在测试代码里直接使用LocationManager.addTestProvider()LocationManager.setTestProviderLocation()

注意:模拟定位需要应用拥有android.permission.ACCESS_MOCK_LOCATION权限。在Android 10及以上,这个权限只能授予系统应用或调试应用。所以,建议只在debug构建中使用。

来看一个完整的模拟定位示例:

public class MockLocationHelper {
    private final LocationManager locationManager;
    private final String providerName = LocationManager.GPS_PROVIDER;

    public MockLocationHelper(Context context) {
        locationManager = (LocationManager)
                context.getSystemService(Context.LOCATION_SERVICE);
    }

    public void enableMockLocation() {
        locationManager.addTestProvider(
                providerName,
                false,   // requiresNetwork
                false,   // requiresSatellite
                false,   // requiresCell
                false,   // hasMonetaryCost
                false,   // supportsAltitude
                false,   // supportsSpeed
                false,   // supportsBearing
                android.location.Criteria.POWER_LOW,
                android.location.Criteria.ACCURACY_FINE
        );
        locationManager.setTestProviderEnabled(providerName, true);
    }

    public void setMockLocation(double latitude, double longitude) {
        Location mockLocation = new Location(providerName);
        mockLocation.setLatitude(latitude);
        mockLocation.setLongitude(longitude);
        mockLocation.setAccuracy(5.0f);
        mockLocation.setTime(System.currentTimeMillis());
        mockLocation.setElapsedRealtimeNanos(
                SystemClock.elapsedRealtimeNanos());

        locationManager.setTestProviderLocation(providerName, mockLocation);
    }

    public void disableMockLocation() {
        locationManager.removeTestProvider(providerName);
    }
}

我在项目中遇到过一个问题:模拟定位设置后,监听器收不到回调。后来发现是因为没有在AndroidManifest.xml里声明ACCESS_MOCK_LOCATION权限。虽然这个权限在debug模式下会自动授予,但显式声明一下更保险。

小技巧:你可以写一个自动化脚本,在每次测试前调用enableMockLocation(),然后循环发送一组预设的坐标序列,模拟一条行走路线。这样就能测试「位置变化」场景下的业务逻辑了。

27.3 UI自动化测试:模拟用户操作

单元测试和模拟定位都搞定了,但还有一个关键问题:用户界面上的定位按钮、权限弹窗、定位状态提示——这些交互逻辑怎么测?

答案是UI自动化测试。我推荐使用Espresso或UI Automator。Espresso更适合测试单个Activity内的交互,而UI Automator可以跨应用操作(比如处理系统权限弹窗)。

来看一个典型的测试场景:用户点击「获取位置」按钮,应用请求定位权限,然后显示当前位置。

@RunWith(AndroidJUnit4.class)
@LargeTest
public class LocationActivityTest {

    @Rule
    public ActivityTestRule<LocationActivity> activityRule =
            new ActivityTestRule<>(LocationActivity.class);

    @Test
    public void testRequestLocation() {
        // 点击获取位置按钮
        onView(withId(R.id.btn_get_location))
                .perform(click());

        // 处理权限弹窗(如果出现)
        // 注意:这里假设权限弹窗是系统对话框
        // 实际测试中可能需要用UiAutomator处理
        onView(withText("允许"))
                .check(matches(isDisplayed()))
                .perform(click());

        // 等待定位结果
        onView(withId(R.id.tv_location))
                .check(matches(withText(containsString("纬度"))));
    }
}

你可能会问:UI自动化测试里,定位数据从哪来?嗯,这里就要把MockLocationProvider和UI测试结合起来。在测试的@Before方法里启动模拟定位,然后在测试用例里发送预设坐标。这样UI就能收到真实的定位回调,而不用依赖硬件。

最佳实践:把模拟定位的启动/停止逻辑封装在测试基类里。每个测试用例只需要关心「发送什么坐标」和「验证什么结果」。

27.4 知识体系总览

为了让你更直观地理解这三层测试的关系,我画了一张图:

定位测试三层架构 JUnit 单元测试 测试纯逻辑:距离计算、地理围栏判断、NMEA解析 不依赖Android框架,运行在JVM上,速度快 MockLocationProvider 模拟定位 注入假坐标,模拟GPS/网络定位数据源 需要ACCESS_MOCK_LOCATION权限,建议debug模式使用 UI 自动化测试 Espresso / UI Automator 模拟用户操作 结合MockLocationProvider,实现端到端定位测试 代码层 数据层 交互层

从这张图你可以看到,三层测试是层层递进的。单元测试保证逻辑正确,模拟定位保证数据可控,UI自动化保证交互流畅。三者缺一不可。

27.5 避坑指南

最后,分享几个我踩过的坑:

  • 模拟定位不生效? 检查一下你的测试设备是否开启了「允许模拟位置」。Android 10以上,这个选项在开发者选项里改名叫「选择模拟位置信息应用」。
  • UI测试里权限弹窗处理不好? 我建议用UI Automator来处理系统弹窗,因为Espresso无法跨应用操作。或者,你可以在测试前用GrantPermissionRule预先授予权限。
  • 定位回调延迟? 模拟定位虽然快,但LocationManager的回调仍然是在异步线程里。在UI测试中,记得用IdlingResource等待定位完成,否则会出现「还没定位完就检查结果」的尴尬情况。

我的习惯:在项目的androidTest目录下建一个location包,专门放定位相关的测试代码。再写一个TestLocationProvider基类,把模拟定位的启动、停止、坐标发送都封装好。这样每个测试用例只需要写几行代码,就能模拟各种定位场景。

定位测试,说白了就是「在可控的环境里,验证不可控的行为」。单元测试、模拟定位、UI自动化,这三板斧用好了,你就能在办公室里把定位功能测个八九不离十。当然,真机外场测试还是不能省——但那是上线前的事了。


公众号:蓝海资料掘金营,微信deep3321