地図を表示して自身の地理的位置(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) // 上記の関数で変更した位置をテスト }