スケジュール管理やアラーム機能を実装するアプリのテストで、端末の時刻を自由に変更できたら便利です。なので、端末の時刻を変更するテストルールを作成してみました。
この記事は「Android端末の時刻を変更するJUnitテストルール(Api23~34対応)」で改訂されました。
目次
時刻を変更する方法
dateツールを使う
adb shellコマンドのdateツールを使って変更します。
使い方は次の通りです。[SET]があれば設定し、無ければ現在時刻を表示します。
usage: date [-u] [-r FILE] [-d DATE] [+DISPLAY_FORMAT] [-D SET_FORMAT] [SET]
Set/get the current date/time. With no SET shows the current date.
Default SET format is “MMDDhhmm[[CC]YY][.ss]”
※MM … 月、DD … 日、hh … 時、mm … 分
例えば、12月31日23時59分に変更してみましょう。
Thu Dec 31 23:59:00 GMT 2020
# date
Thu Dec 31 23:59:01 GMT 2020
- Root権限が必要
(UiAutomatorのUiDeviceからadb shellを発行した場合、Rootになれない)
SettingsアプリをGUI操作
Settingsアプリを開いてGUIの操作で変更します。つまり、ユーザが手で操作するのと同じことをプログラム的に行わせようというわけです。
private fun _SetSystemTime(hour: Int, min: Int) { val uiDevice = UiDevice.getInstance( InstrumentationRegistry.getInstrumentation() ) val setTime = when(Build.VERSION.SDK_INT) { in 21..28 -> "Set time" 29 -> "Time" else -> "Set time" } val automatic = when(Build.VERSION.SDK_INT) { in 21..28 -> "Automatic date & time" 29 -> "Use network-provided time" else -> "Automatic date & time" } // Settingアプリの「Date & time」設定ページを開く _ShowSettingsDateTime() // 時刻の自動(Network)設定を無効化 if (!_FindObjByText(setTime).isEnabled) { _FindObjByText(automatic).click() } // 時刻設定のPickerを開く _FindObjByText(setTime).click() // Am or Pmを指定 val _AmPm = hour / 12 % 2 if (_AmPm == 0) _FindObjByText("AM").click() if (_AmPm == 1) _FindObjByText("PM").click() // Picker上で時を指定 val _Hour = (hour + 11) % 12 + 1 _ClickDialPicker(_Hour) // Picker上で分を指定 val _Min = min / 5 % 12 * 5 _ClickDialPicker(_Min) // OKの押下で指定の有効化 _FindObjByText("OK").click() // 「Date & time」設定ページに戻るのを待って、Settingsアプリを閉じる _FindObjByText(setTime) uiDevice.pressBack() } private fun _FindObjByRes(resource: String): UiObject2 { val uiDevice = UiDevice.getInstance( InstrumentationRegistry.getInstrumentation()) return uiDevice.wait(Until.findObject(By.res(resource)), 2000) ?: throw UiObjectNotFoundException("ResourceName:\"${resource}\"") } private fun _FindObjByText(text: String): UiObject2 { val uiDevice = UiDevice.getInstance( InstrumentationRegistry.getInstrumentation()) return uiDevice.wait(Until.findObject(By.text(text)), 2000) ?: throw UiObjectNotFoundException("Text:\"${text}\"") } private fun _ClickDialPicker(num: Int) { val obj = _FindObjByRes("android:id/radial_picker") val objList = obj.children for (o in objList) { if (Integer.valueOf(o.contentDescription) == num) { o.click() } } } private fun _ShowSettingsDateTime() { val context = InstrumentationRegistry.getInstrumentation().context val intent = Intent(Settings.ACTION_DATE_SETTINGS) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK context.startActivity(intent) }
private void _SetSystemTime(int hour, int min) throws UiObjectNotFoundException { UiDevice _UiDevice = UiDevice.getInstance( InstrumentationRegistry.getInstrumentation() ); String _SetTime = "Set time"; String _Automatic = "Automatic date & time"; switch (Build.VERSION.SDK_INT) { case 21-28: _SetTime = "Set time"; _Automatic = "Automatic date & time"; break; case 29: _SetTime = "Time"; _Automatic = "Use network-provided time"; break; default: break; } // Settingアプリの「Date & time」設定ページを開く _ShowSettingsDateTime(); // 時刻の自動(Network)設定を無効化 if (!_FindObjByText(_SetTime).isEnabled()) { _FindObjByText(_Automatic).click(); } // 時刻設定のPickerを開く _FindObjByText(_SetTime).click(); // Am or Pmを指定 int _AmPm = (hour / 12) % 2; if(_AmPm == 0) _FindObjByText("AM").click(); if(_AmPm == 1) _FindObjByText("PM").click(); // Picker上で時を指定 int _Hour = ((hour + 11) % 12) + 1; _ClickDialPicker(_Hour); // Picker上で分を指定 int _Min = ((min / 5) % 12) * 5; _ClickDialPicker(_Min); // OKの押下で指定の有効化 _FindObjByText("OK").click(); // 「Date & time」設定ページに戻るのを待って、Settingsアプリを閉じる _FindObjByText(_SetTime); _UiDevice.pressBack(); } private UiObject2 _FindObjByRes(String resource) throws UiObjectNotFoundException { UiDevice _UiDevice = UiDevice.getInstance( InstrumentationRegistry.getInstrumentation()); UiObject2 _Obj= _UiDevice.wait(Until.findObject(By.res(resource)), 2000); if(_Obj == null) throw new UiObjectNotFoundException( String.format("ResourceName:\"%s\"", resource)); return _Obj; } private UiObject2 _FindObjByText(String text) throws UiObjectNotFoundException { UiDevice _UiDevice = UiDevice.getInstance( InstrumentationRegistry.getInstrumentation()); UiObject2 _Obj= _UiDevice.wait(Until.findObject(By.text(text)), 2000); if(_Obj == null) throw new UiObjectNotFoundException( String.format("Text:\"%s\"", text)); return _Obj; } private void _ClickDialPicker(int num) throws UiObjectNotFoundException { UiObject2 _Obj = _FindObjByRes("android:id/radial_picker"); List<UiObject2> _ObjList = _Obj.getChildren(); for(UiObject2 _O: _ObjList) { if(Integer.valueOf(_O.getContentDescription()) == num) { _O.click(); } } } private void _ShowSettingsDateTime() { Context _Context = InstrumentationRegistry.getInstrumentation().getContext(); Intent _Intent = new Intent(Settings.ACTION_DATE_SETTINGS); _Intent.setFlags(FLAG_ACTIVITY_NEW_TASK); _Context.startActivity(_Intent); }
※上記のコードはLanguage⇒Englishのみ対応
※上記のコードは12H表記のみ対応(24H表記は未対応)
操作はAVD(Android Virtual Device)上で行う場合を基準にしています。
API≧29で項目名が変更になっているため、操作対象のObjectを見つける時のテキストをプログラム中で切り替える必要がありました。
API<29 | API≧29 |
---|---|
時刻の設定はダイアル式になっています。ドラッグした位置からスクリーン上をスライドさせることで細かな指定ができます。でも、プログラム中から行うのが困難だったのでダイアルの数字を検出してクリックしています。
時設定のダイアル | 分設定のダイアル |
---|---|
ただ、この方法には注意点があります。
- GUI操作の動作が遅い
- 操作方法はAVDが基準、カスタマイズされていると動かない
- API==21,22の場合、UiDevice#findObjectでObjectが見つけられない(原因不明)
Automatic(Networkから取得)へ変更する方法
SettingsアプリをGUI操作
Settingsアプリを開いてGUIの操作で変更します。「時刻を変更する方法」と同様です。
Automaticへ変更すると直ちにNetworkから時刻を取得してくれます。
private fun _SetAutomatic() { val uiDevice = UiDevice.getInstance( InstrumentationRegistry.getInstrumentation() ) val setTime = when(Build.VERSION.SDK_INT) { in 21..28 -> "Set time" 29 -> "Time" else -> "Set time" } val automatic = when(Build.VERSION.SDK_INT) { in 21..28 -> "Automatic date & time" 29 -> "Use network-provided time" else -> "Automatic date & time" } // Settingの「Date & time」設定ページを開く _ShowSettingsDateTime() // 時刻の自動(Network)設定を有効化 if (_FindObjByText(setTime).isEnabled) { _FindObjByText(automatic).click() } // Settingsアプリを閉じる uiDevice.pressBack() }
private void _SetAutomatic() throws UiObjectNotFoundException { UiDevice _UiDevice = UiDevice.getInstance( InstrumentationRegistry.getInstrumentation() ); String _SetTime = "Set time"; String _Automatic = "Automatic date & time"; switch (Build.VERSION.SDK_INT) { case 21-28: _SetTime = "Set time"; _Automatic = "Automatic date & time"; break; case 29: _SetTime = "Time"; _Automatic = "Use network-provided time"; break; default: break; } // Settingの「Date & time」設定ページを開く _ShowSettingsDateTime(); // 時刻の自動(Network)設定を有効化 if (_FindObjByText(_SetTime).isEnabled()) { _FindObjByText(_Automatic).click(); } // Settingsアプリを閉じる _UiDevice.pressBack(); }
テストルールの作成
「時刻を変更する方法」の注意点を考慮して、テストルールは次のような仕様にしました。
- API≧23で動作
- SettingsアプリをGUI操作して変更を行う
- 分の指定は5分間隔(0,5,10, …,55)、中間値(23や44など)は5の倍数へ切り捨て
- 「分の指定>60」の場合は60の倍数で切り詰める(例:72であれば12になる)
- 「時の指定>24」の場合は24の倍数で切る詰める(例:26であれば2になる)
- テスト後にAutomaticへ変更する
@TargetApi(23) class SysTimeTestRule(var hour: Int, var min: Int) : TestRule { // ----- ルール本体 ------------------------------------------------------- override fun apply(base: Statement, description: Description): Statement { return object : Statement() { override fun evaluate() { try { setSysTime(hour, min) // 前処理 base.evaluate() // テストの実施 } finally { setAutomatic() // 後処理 } } } } fun setAutomatic() { if (hour >= 0 || min >= 0) { _SetAutomatic() } hour = -1 min = -1 } fun setSysTime(hour: Int, min: Int) { if (hour >= 0 && min >= 0) { _SetSystemTime(hour, min) } this.hour = hour this.min = min } ... }
@TargetApi(23) public class SysTimeTestRule implements TestRule { private int mHour; private int mMin; // ----- コンストラクタ --------------------------------------------------- public SysTimeTestRule(int hour, int min) { mHour = hour; mMin = min; } // ----- Rule本体 --------------------------------------------------------- @Override public Statement apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { try { setSysTime(mHour, mMin); // 前処理 base.evaluate(); // テストの実施 } finally { setAutomatic(); // 後処理 } } }; } public void setAutomatic() throws UiObjectNotFoundException { if(mHour >= 0 || mMin >= 0) { _SetAutomatic(); } mHour = -1; mMin =-1; } public void setSysTime(int hour, int min) throws UiObjectNotFoundException { if(hour >= 0 && min >= 0) { _SetSystemTime(hour, min); } mHour = hour; mMin = min; } ... }
テストルールの使用方法
使用方法はその他のテストルールと変わりません。@Ruleを付けてプロパティ(Javaの場合はフィールド)を宣言するだけです。
テストの途中でもsetSysTimeを用いて時刻の変更が可能です。
class Rule_SystemTime_Test { @get:Rule var timeRule = SysTimeTestRule(11, 59) // ------------------------------------------------------------------------ @Test fun 時刻の指定_1159() { val _Calendar = Calendar.getInstance() Truth.assertThat(_Calendar[Calendar.HOUR_OF_DAY]).isEqualTo(11) Truth.assertThat(_Calendar[Calendar.MINUTE]).isEqualTo(55) } @Test fun 時刻の指定_午前0916() { timeRule.setSysTime(9, 16) val _Calendar = Calendar.getInstance() Truth.assertThat(_Calendar[Calendar.HOUR_OF_DAY]).isEqualTo(9) Truth.assertThat(_Calendar[Calendar.MINUTE]).isEqualTo(15) } }
public class Rule_SystemTime_Test { @Rule public SysTimeTestRule mTimeRule = new SysTimeTestRule(11, 59); // ------------------------------------------------------------------------ @Test public void 時刻の指定_1159() throws Exception { Calendar _Calendar = Calendar.getInstance(); assertThat(_Calendar.get(Calendar.HOUR_OF_DAY)).isEqualTo(11); assertThat(_Calendar.get(Calendar.MINUTE)).isEqualTo(55); } @Test public void 時刻の指定_午前0916() throws Exception { mTimeRule.setSysTime(9, 16); Calendar _Calendar = Calendar.getInstance(); assertThat(_Calendar.get(Calendar.HOUR_OF_DAY)).isEqualTo(9); assertThat(_Calendar.get(Calendar.MINUTE)).isEqualTo(15); } }