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 知识体系总览
为了让你更直观地理解这三层测试的关系,我画了一张图:
从这张图你可以看到,三层测试是层层递进的。单元测试保证逻辑正确,模拟定位保证数据可控,UI自动化保证交互流畅。三者缺一不可。
27.5 避坑指南
最后,分享几个我踩过的坑:
- 模拟定位不生效? 检查一下你的测试设备是否开启了「允许模拟位置」。Android 10以上,这个选项在开发者选项里改名叫「选择模拟位置信息应用」。
- UI测试里权限弹窗处理不好? 我建议用UI Automator来处理系统弹窗,因为Espresso无法跨应用操作。或者,你可以在测试前用
GrantPermissionRule预先授予权限。 - 定位回调延迟? 模拟定位虽然快,但LocationManager的回调仍然是在异步线程里。在UI测试中,记得用
IdlingResource等待定位完成,否则会出现「还没定位完就检查结果」的尴尬情况。
我的习惯:在项目的androidTest目录下建一个location包,专门放定位相关的测试代码。再写一个TestLocationProvider基类,把模拟定位的启动、停止、坐标发送都封装好。这样每个测试用例只需要写几行代码,就能模拟各种定位场景。
定位测试,说白了就是「在可控的环境里,验证不可控的行为」。单元测试、模拟定位、UI自动化,这三板斧用好了,你就能在办公室里把定位功能测个八九不离十。当然,真机外场测试还是不能省——但那是上线前的事了。
公众号:蓝海资料掘金营,微信deep3321