Android测试无需第三方应用模拟定位的方法及权限问题解决
Android Instrumentation测试模拟定位问题解决方案
问题描述
我需要给一款依赖实时定位的导航应用做Instrumentation测试,想在不借助第三方模拟定位工具的前提下,在Android设备上模拟当前位置。但尝试通过GrantPermissionRule申请android.permission.ACCESS_MOCK_LOCATION时,出现报错:
Permission: android.permission.ACCESS_MOCK_LOCATION cannot be granted!
我的测试代码如下:
@LargeTest @RunWith(AndroidJUnit4ClassRunner::class) class SpeedTTSWarningTrackingInstrumentedTests { // ------------------------------------------------------------------------------------------------- private var lm : LocationManager? = null companion object { private val appContext: Context = getApplicationContext() private val mainIntent = Intent(appContext, MainActivity::class.java) private var mActivityIdlingResource: IdlingResource? = null } // ------------------------------------------------------------------------------------------------- @Rule(order = 0) @JvmField val permissionRule: GrantPermissionRule = GrantPermissionRule.grant( Manifest.permission.INTERNET, Manifest.permission.ACCESS_NETWORK_STATE, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION, "android.permission.ACCESS_MOCK_LOCATION" ) @Rule(order = 1) @JvmField val activityScenarioRule = ActivityScenarioRule<MainActivity>(mainIntent) // ------------------------------------------------------------------------------------------------- @Before fun beforeTests(){ activityScenarioRule.scenario.moveToState(Lifecycle.State.RESUMED) runBlocking { delay(2000) } activityScenarioRule.scenario.onActivity { activity -> mActivityIdlingResource = activity.getActivityIdlingResource() IdlingRegistry.getInstance().register(mActivityIdlingResource) } } @After fun closeActivity(){ activityScenarioRule.scenario.close() } // ------------------------------------------------------------------------------------------------- @Test fun changeLocation(){ runBlocking { delay(10000) } mockLocation(45.661936, 25.608026) runBlocking { delay(10000) } } // ------------------------------------------------------------------------------------------------- private fun mockLocation(lat: Double, lon: Double) { lm = getSystemService(appContext,LocationManager::class.java) val mocLocationProvider = LocationManager.FUSED_PROVIDER lm!!.addTestProvider( mocLocationProvider, false, false, false, false, true, true, true, 3, 2 ) lm!!.setTestProviderEnabled(mocLocationProvider, true) val mockLocation = Location(mocLocationProvider) // a string mockLocation.latitude = lat // double mockLocation.longitude = lon mockLocation.time = System.currentTimeMillis() mockLocation.accuracy = 1f if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { mockLocation.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() } lm!!.setTestProviderLocation(mocLocationProvider, mockLocation) } }
我有几个疑问:
- 我的代码哪里出错了?该怎么修复?
- 已知
ACCESS_MOCK_LOCATION从API 23开始被移除,现在模拟定位应该用什么方式? - 我试过用Mock Location Provider、
GrantPermissionRule,还单独创建了debug版本的AndroidManifest,这些方法是否正确?
问题根源与解决方案
1. 权限问题的核心原因
API 23(Android 6.0)之后,ACCESS_MOCK_LOCATION权限被彻底移除,模拟定位的授权不再通过运行时权限申请流程,而是需要:
- 开启设备开发者选项中的允许模拟位置开关
- 在开发者选项的选择模拟位置应用中,指定你的测试APP作为模拟位置的提供者
- 确保APP已经申请了
ACCESS_FINE_LOCATION或ACCESS_COARSE_LOCATION权限(这两个权限仍需通过GrantPermissionRule申请)
你用GrantPermissionRule申请ACCESS_MOCK_LOCATION必然失败,因为这个权限已经不存在于高版本系统中。
2. 当前代码的错误点
- 错误使用Fused Provider:
LocationManager.FUSED_PROVIDER是系统内置的融合定位服务,不能直接作为测试Provider添加,必须自定义一个专属的测试Provider名称(比如"TEST_PROVIDER") - 未清理测试Provider:测试前后没有移除测试Provider,可能导致重复添加报错
- 固定延迟不可靠:用
runBlocking { delay() }等待页面加载,不如用IdlingResource监听页面状态更稳定
3. 正确的实现步骤
步骤一:配置Debug版本Manifest
在src/debug/AndroidManifest.xml中添加以下配置(只作用于测试环境,不影响正式包):
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- 保留旧权限是为了兼容API 22及以下,高版本会自动忽略 --> <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" tools:ignore="MockLocationPermissions" /> <application> <!-- 可选:声明测试用定位Provider,让配置更规范 --> <provider android:name="android.location.LocationProvider" android:authorities="com.your.package.test.location" android:exported="false" /> </application>
步骤二:设备/模拟器前置配置
- 打开开发者选项(连续点击系统设置中的「版本号」7次)
- 找到「允许模拟位置」并开启
- 在「选择模拟位置应用」中,选中你的测试APP(通常包名带有
.test后缀)
步骤三:修改测试代码
调整后的代码如下,修复了Provider使用问题,并添加了测试前后的资源清理:
@LargeTest @RunWith(AndroidJUnit4ClassRunner::class) class SpeedTTSWarningTrackingInstrumentedTests { private var lm: LocationManager? = null companion object { private val appContext: Context = getApplicationContext() private val mainIntent = Intent(appContext, MainActivity::class.java) private var mActivityIdlingResource: IdlingResource? = null // 自定义测试定位Provider名称 private const val TEST_LOC_PROVIDER = "TEST_LOC_PROVIDER" } // 移除无效的ACCESS_MOCK_LOCATION权限申请 @Rule(order = 0) @JvmField val permissionRule: GrantPermissionRule = GrantPermissionRule.grant( Manifest.permission.INTERNET, Manifest.permission.ACCESS_NETWORK_STATE, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION ) @Rule(order = 1) @JvmField val activityScenarioRule = ActivityScenarioRule<MainActivity>(mainIntent) @Before fun setupTests() { activityScenarioRule.scenario.moveToState(Lifecycle.State.RESUMED) // 用IdlingResource替代固定延迟,更可靠 activityScenarioRule.scenario.onActivity { activity -> mActivityIdlingResource = activity.getActivityIdlingResource() IdlingRegistry.getInstance().register(mActivityIdlingResource) } // 初始化LocationManager并准备测试Provider lm = getSystemService(appContext, LocationManager::class.java) lm?.apply { // 先移除已存在的测试Provider,避免重复添加报错 getProvider(TEST_LOC_PROVIDER)?.let { removeTestProvider(TEST_LOC_PROVIDER) } // 添加自定义测试Provider addTestProvider( TEST_LOC_PROVIDER, false, false, false, false, true, true, true, 0, 5 ) setTestProviderEnabled(TEST_LOC_PROVIDER, true) } } @After fun teardownTests() { // 清理测试资源 lm?.apply { setTestProviderEnabled(TEST_LOC_PROVIDER, false) removeTestProvider(TEST_LOC_PROVIDER) } mActivityIdlingResource?.let { IdlingRegistry.getInstance().unregister(it) } activityScenarioRule.scenario.close() } @Test fun testLocationChange() { // 等待页面初始化完成(依赖IdlingResource) mockLocation(45.661936, 25.608026) // 这里可以添加断言,验证UI是否正确响应了位置变化 } private fun mockLocation(lat: Double, lon: Double) { lm?.let { locationManager -> val mockLocation = Location(TEST_LOC_PROVIDER).apply { latitude = lat longitude = lon time = System.currentTimeMillis() accuracy = 1f if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { provider = TEST_LOC_PROVIDER } } // 设置模拟位置 locationManager.setTestProviderLocation(TEST_LOC_PROVIDER, mockLocation) // 发送广播通知系统定位更新 locationManager.sendBroadcast(Intent(LocationManager.PROVIDERS_CHANGED_ACTION)) } } }
4. 关于你尝试的方法的正确性
- Mock Location Provider:思路正确,但不能直接使用系统的Fused Provider,必须自定义测试Provider
- GrantPermissionRule:只适用于普通危险权限,模拟位置的授权不通过这个流程,所以申请
ACCESS_MOCK_LOCATION无效,应该移除 - debug AndroidManifest:是正确的做法,通过debug专属配置隔离测试环境和正式环境,非常推荐
额外技巧
如果使用模拟器测试,可以直接用ADB命令快速设置模拟位置,无需编写代码:
adb shell am set-location -e 45.661936 25.608026
内容的提问来源于stack exchange,提问作者Purple6666
相关产品推荐
相关产品推荐

