Android端末の地理的位置を変更するJUnitテストルール

投稿日:  更新日:

地図を表示して自身の地理的位置(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が変更されます。

AndroidEmulatorで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フォルダのファイルが採用される仕組みになっています。

Debugコンパイルの時に有効なAndroidManifest.xml

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

SettingsでAllowMockLocationsを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」でアプリを選択します。

SettingsでAllowMockLocationsをOnにする

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)
        // 上記の関数で変更した位置をテスト
    }
スポンサーリンク
スポンサーリンク