地図を表示して自身の地理的位置(Geolocation)を管理するアプリのテストで、疑似的に端末の位置が変更できたら便利です。なので、端末の位置を変更するテストルールを作成してみました。
目次
端末は常に自身の地理的位置情報をもつ
端末の地理的位置の取得方法は(1)公衆無線LANのアクセスポイント、(2)携帯電話の基地局、(3)GPS、などがあります。後者ほど位置精度が高くなるのが普通です。
得られた位置情報は「最後に端末が存在ていた位置(Last location)」として端末が保持しています。そして、新しい位置情報が得られるたびにLast locationを更新しています。
マップアプリの起動直後に、自分の位置が直ちに地図上へ表示できるのはLast locationがあるためです。
今回作成するテストルールは、このLast locationを疑似位置(Mock location)へ変更します。
端末の地理的位置を変更する方法
Android Emulatorのコントロールパネルを使う
Android Emulatorのコントロールパネルを開きます。
リスト最上部のLocationタブを開き、地図上をダブルクリック(または、検索バーで名称を検索)すれば位置がマークされます。
後は「SET LOCATION」ボタンの押下でLast locationが変更されます。

例はLast locationを「レインボーブリッジ」に変更しています。
Android Emulatorのコンソールを使う
Android Emulatorはポートに接続すると制御コンソールが開くようになっています。
コンソールからgeoコマンドを使って位置情報の変更が可能です。
ただし、geoコマンドを使うためには認証が必要です。認証コードはホームフォルダ(ホームディレクトリ)のファイルに記載されています。
> adb devices ... Emulatorのポート番号を調べる List of devices attached emulator-5554 device > telnet localhost 5554 ... telnetでポート(5554)へ接続 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Android Console: Authentication required Android Console: type 'auth <auth_token>' to authenticate Android Console: you can find your <auth_token> in 'C:\Users\ユーザ名\.emulator_console_auth_token' .... 認証コードの所在 OK auth 認証コード ... geoコマンドの利用は認証が必要 Android Console: type 'help' for a list of commands OK help geo ... 認証後、geoが使える(helpで確認した) allows you to change Geo-related settings, or to send GPS NMEA sentences available sub-commands: geo nmea send a GPS NMEA sentence geo fix send a simple GPS fix geo gnss send a GNSS sentence OK geo fix 139.7631 35.6366 ... レインボーブリッジのLocationを設定 OK exit ... 接続を終了 Connection closed by foreign host.
例はLast locationを「レインボーブリッジ」に変更しています。
FusedLocationProviderClientのMockモード
FusedLocationProviderClient(以下Client)はMockモードというモードを持っています。このMockモードを使うと疑似位置がClientに設定できます。
このモードを使うために2つの操作が必要です。
(1)アプリにMockモードを許可
(2)ClientをMockモードに設定、疑似位置を登録
以降、ClientからのLast locationの取得結果が疑似位置になります。
API<23
(1)アプリにMockモードを許可
AndroidManifest.xmlへACCESS_MOCK_LOCATIONパーミッションを記述します。
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="パッケージ名">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"
tools:ignore="ProtectedPermissions" />
<application
...
</application>
</manifest>
ただし、Mockモードはテストでのみ必要な機能であり、アプリの実使用では必要ない機能です。
よって、Debug時のみパーミッションが有効になるように、mainフォルダと並列にdebugフォルダを作成して、その中にパーミッションを記述したAndroidManifest.xmlを配置します。
Build Valiant = “Debug”の場合はdebugフォルダのファイルが採用される仕組みになっています。

次に端末のSettingsアプリから「Developer Options」を開いて「Allow mock locations」をOnにします。

これで、Mockモードが許可されます。
(2)ClientをMockモードに設定、疑似位置を登録
FusedLocationProviderClientを取得して、Mockモードをtureにし、Mock locationを設定します。
注意点はsetElapsedRealtimeNanos()の部分です。
Elapsed Realtimeはこの位置が取得された時間ですが、起点がAndroid Systemが起動してからの時間になります。設定したMock locationをLast locationとして振舞わせるために必要です。
fun setGeolocation(longi: Double, lati: Double) {
if(longi > 0 && lati > 0) {
// 疑似ロケーションを作成
val _location = Location("mock") // プロバイダ名に"mock"を指定
_location.longitude = longi
_location.latitude = lati
_location.accuracy = 1.0f
_location.time = System.currentTimeMillis()
_location.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
// 疑似ロケーションを登録
val _client = LocationServices.getFusedLocationProviderClient(context)
_client.setMockMode(true)
_client.setMockLocation(_location)
.addOnSuccessListener {
Log.d("GeoLocationRule", "The mock geolocation succeeded.")
}
.addOnFailureListener {
Log.d("GeoLocationRule", "The mock geolocation failed.")
}
}
}
API≧23
(1)アプリにMockモードを許可(その1:Settingsアプリで)
AndroidManifest.xmlへACCESS_MOCK_LOCATIONパーミッションを記述します。
プログラム中でユーザへパーミッションの要求をする必要はありません。記述するだけでよいです。
※以下、API<23と同じ
次に端末のSettingsアプリから「Developer Options」を開いて「Select mock location app」でアプリを選択します。

AndroidManifest.xmlへACCESS_MOCK_LOCATIONパーミッションが記述されていないアプリは、選択の対象に表示されません。
(1)アプリにMockモードを許可(その2:adb shell appopsで)
API≧23はappopsコマンドが用意されていて、Mockモードの許可の取得がadb shellから行えます。
appops set パッケージ名 android:mock_location allow ... 許可する appops set パッケージ名 android:mock_location deny ... 許可しない appset get パッケージ名 android:mock_location ... 状態を取得
UiDeviceを使えばadb shellが発行できるので、テストプログラムから操作ができます。
private val uiDevice: UiDevice
private val context: Context
private val setCmd: String
private val getCmd: String
init {
uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
context = InstrumentationRegistry.getInstrumentation().targetContext
setCmd = "appops set ${context.packageName} android:mock_location"
getCmd = "appops get ${context.packageName} android:mock_location"
}
...
private fun allowMock() {
if(Build.VERSION.SDK_INT >= 23) {
uiDevice.executeShellCommand("${setCmd} allow") // appopsはAPI≧23が必要
val _status = uiDevice.executeShellCommand(getCmd) // 同上
Log.d("GeoLocationRule", "Status = ${_status}")
}
}
private fun denyMock() {
if(Build.VERSION.SDK_INT >= 23) {
uiDevice.executeShellCommand("${setCmd} deny") // appopsはAPI≧23が必要
val _status = uiDevice.executeShellCommand(getCmd) // 同上
Log.d("GeoLocationRule", "Status = ${_status}")
}
}
appopsを使う場合、AndroidManifest.xmlへACCESS_MOCK_LOCATIONパーミッションの記述は必要ありません。
(2)ClientをMockモードに設定、疑似位置を登録
※API<23と同じ、省略
テストルールの作成
テストルールは次のような仕様にしました。
- API<23はSettingアプリでMockモードを許可する
- API≧23はadb shell appoptでMockモードを許可する
- テスト後にMockモードを解除する
従って、API<23ではAndroidManifest.xmlへACCESS_MOCK_LOCATIONパーミッションの記述が必要になります。
class GeolocationRule(val longitude: Double = -1.0, val latitude: Double = -1.0) : TestRule {
...
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
try {
allowMock() // 前処理
setGeolocation(longitude, latitude)
base.evaluate() // テストの実施
} finally {
denyMock() // 後処理
}
}
}
}
private fun allowMock() {
...
}
private fun denyMock() {
...
}
fun setGeolocation(longi: Double, lati: Double) {
...
}
}
テストルールの使用方法
使用方法はその他のテストルールと変わりません。@Ruleを付けてプロパティを宣言するだけです。
テスト途中でもsetGeolocationを用いてLast locationの変更が可能です。
class SampleTest {
@get:Rule
var locationRule = GeolocationRule(TOKYO_STATION_LONG, TOKYO_STATION_LATI)
@get:Rule
var activityRule = ActivityScenarioRule(MainActivity::class.java)
// ※ActiviryTestRuleは非推奨になりました
@Before
fun setUp() {}
@After
fun tearDown() {}
@Test
fun sammple_test() {
// コンストラクタの引数で指定した位置をテスト
locationRule.setGeolocation(JAPAN_MT_FUJI_LONG, JAPAN_MT_FUJI_LATI)
// 上記の関数で変更した位置をテスト
}
