スケジュール管理やアラーム機能を実装するアプリのテストで、端末の日付を自由に変更できたら便利です。なので、端末の日付を変更するテストルールを作成してみました。
※この記事は「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 _SetSystemDate(year: Int, month: Int, day: Int) { val uiDevice = UiDevice.getInstance( InstrumentationRegistry.getInstrumentation() ) val setDate = when(Build.VERSION.SDK_INT) { in 21..28 -> "Set date" 29 -> "Date" else -> "Set date" } val automatic = when(Build.VERSION.SDK_INT) { in 21..28 -> "Automatic date & time" 29 -> "Use network-provided time" else -> "Automatic date & time" } val targetCal = Calendar.getInstance() targetCal.clear() targetCal[Calendar.YEAR] = year targetCal[Calendar.MONTH] = month targetCal[Calendar.DAY_OF_MONTH] = day val targetYear = targetCal[Calendar.YEAR] val targetMonth = targetCal[Calendar.MONTH] val targetDay = targetCal[Calendar.DAY_OF_MONTH] val currentCal = Calendar.getInstance() val currentYear = currentCal[Calendar.YEAR] val currentMonth = currentCal[Calendar.MONTH] // Settingアプリの「Date & time」設定ページを開く _ShowSettingsDateTime() // 時刻の自動(Network)設定を無効化 if (!_FindObjByText(setDate).isEnabled) { _FindObjByText(automatic).click() } // 日付設定のPickerを開く _FindObjByText(setDate).click() // 年月の変更 val step = (targetYear - currentYear) * 12 + (targetMonth - currentMonth) for (i in 0 until Math.abs(step)) { if (step > 0) _FindObjByRes("android:id/next").click() else _FindObjByRes("android:id/prev").click() } // 日の変更 try { _FindObjByText(targetDay.toString()).click() // OKの押下で指定の有効化 _FindObjByText("OK").click() } catch (e: UiObjectNotFoundException) { // CANCELの押下で指定の無効化 _FindObjByRes("android:id/button2").click() // CANCELボタン throw UiObjectNotFoundException( "Date:\"${targetYear}.${targetMonth + 1}.${targetDay}\"") } // 「Date & time」設定ページに戻るのを待って、Settingsアプリを閉じる _FindObjByText(setDate) 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 _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 _SetSystemDate(int year, int month, int day) throws UiObjectNotFoundException { UiDevice _UiDevice = UiDevice.getInstance( InstrumentationRegistry.getInstrumentation() ); String _SetDate = "Set date"; String _Automatic = "Automatic date & time"; switch (Build.VERSION.SDK_INT) { case 21-28: _SetDate = "Set date"; _Automatic = "Automatic date & time"; break; case 29: _SetDate = "Date"; _Automatic = "Use network-provided time"; break; default: break; } Calendar _TargetCal = Calendar.getInstance(); _TargetCal.clear(); _TargetCal.set(Calendar.YEAR, year); _TargetCal.set(Calendar.MONTH, month); _TargetCal.set(Calendar.DAY_OF_MONTH, day); int _TargetYear = _TargetCal.get(Calendar.YEAR); int _TargetMonth = _TargetCal.get(Calendar.MONTH); int _TargetDay = _TargetCal.get(Calendar.DAY_OF_MONTH); Calendar _CurrentCal = Calendar.getInstance(); int _CurrentYear = _CurrentCal.get(Calendar.YEAR); int _CurrentMonth = _CurrentCal.get(Calendar.MONTH); // Settingアプリの「Date & time」設定ページを開く _ShowSettingsDateTime(); // 時刻の自動(Network)設定を無効化 if (!_FindObjByText(_SetDate).isEnabled()) { _FindObjByText(_Automatic).click(); } // 日付設定のPickerを開く _FindObjByText(_SetDate).click(); // 年月の変更 int _Step = (_TargetYear - _CurrentYear) * 12 + (_TargetMonth - _CurrentMonth); for(int i = 0; i < Math.abs(_Step); i++) { if(_Step > 0) _FindObjByRes("android:id/next").click(); else _FindObjByRes("android:id/prev").click(); } // 日の変更 try { _FindObjByText(String.valueOf(_TargetDay)).click(); // OKの押下で指定の有効化 _FindObjByText("OK").click(); } catch (UiObjectNotFoundException e) { // CANCELの押下で指定の無効化 _FindObjByRes("android:id/button2").click(); // CANCELボタン throw new UiObjectNotFoundException(String.format( "Date:\"%d.%d.%d\"", _TargetYear, _TargetMonth + 1, _TargetDay)); } // 「Date & time」設定ページに戻るのを待って、Settingsアプリを閉じる _FindObjByText(_SetDate); _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 _ShowSettingsDateTime() { Context _Context = InstrumentationRegistry.getInstrumentation().getContext(); Intent _Intent = new Intent(Settings.ACTION_DATE_SETTINGS); _Intent.setFlags(FLAG_ACTIVITY_NEW_TASK); _Context.startActivity(_Intent); }
操作はAVD(Android Virtual Device)上で行う場合を基準にしています。
API≧29で項目名が変更になっているため、操作対象のObjectを見つける時のテキストをプログラム中で切り替える必要がありました。
API<29 | API≧29 |
---|---|
日付の選択はカレンダー式になっています。日付をクリックすることで指定できます。
日付設定のカレンダー(API<23) | 日付設定のカレンダー(API≧23) |
---|---|
- 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 setDate = when(Build.VERSION.SDK_INT) { in 21..28 -> "Set date" 29 -> "Date" else -> "Set date" } 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(setDate).isEnabled) { _FindObjByText(automatic).click() } // Settingsアプリを閉じる uiDevice.pressBack() }
private void _SetAutomatic() throws UiObjectNotFoundException { UiDevice _UiDevice = UiDevice.getInstance( InstrumentationRegistry.getInstrumentation() ); String _SetDate = "Set date"; String _Automatic = "Automatic date & time"; switch (Build.VERSION.SDK_INT) { case 21-28: _SetDate = "Set date"; _Automatic = "Automatic date & time"; break; case 29: _SetDate = "Date"; _Automatic = "Use network-provided time"; break; default: break; } // Settingの「Date & time」設定ページを開く _ShowSettingsDateTime(); // 時刻の自動(Network)設定を有効化 if (_FindObjByText(_SetDate).isEnabled()) { _FindObjByText(_Automatic).click(); } // Settingsアプリを閉じる _UiDevice.pressBack(); }
テストルールの作成
「日付を変更する方法」の注意点を考慮して、テストルールは次のような仕様にしました。
- API≧23で動作
- SettingsアプリをGUI操作して変更を行う
- 年の指定を「2015≦年≦2025」に制限する
- 「月の指定>12」の場合は翌年に繰り上げる(例:14であれば翌年の2月)
- 「日の指定>月末」の場合は翌月に繰り上げる(例:33であれば翌月の2日)
- テスト後にAutomaticへ変更する
@TargetApi(23) class SysDateTestRule(var year: Int, var month: Int, var day: Int) : TestRule { companion object { private const val MIN_YEAR = 2015 private const val MAX_YEAR = 2025 } // ----- Rule本体 --------------------------------------------------------- override fun apply(base: Statement, description: Description): Statement { return object : Statement() { override fun evaluate() { try { setSysDate(year, month, day) // 前処理 base.evaluate() // テストの実施 } finally { setAutomatic() // 後処理 } } } } fun setAutomatic() { if (year >= 0 || month >= 0 || day >= 0) { _SetAutomatic() } year = -1 month = -1 day = -1 } fun setSysDate(year: Int, month: Int, day: Int) { require(MIN_YEAR <= year && year <= MAX_YEAR) { "\"${MIN_YEAR} <= year(${year}) <= ${MAX_YEAR}\"" } if (year >= 0 && month >= 0 && day >= 0) { _SetSystemDate(year, month, day) } this.year = year this.month = month this.day = day } ... }
@TargetApi(23) public class SysDateTestRule implements TestRule { static final private int MIN_YEAR = 2015; static final private int MAX_YEAR = 2025; private int mYear; private int mMonth; private int mDay; // ----- コンストラクタ --------------------------------------------------- public SysDateTestRule(int year, int month, int day) { mYear = year; mMonth = month; mDay = day; } // ----- Rule本体 --------------------------------------------------------- @Override public Statement apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { try { setSysDate(mYear, mMonth, mDay); // 前処理 base.evaluate(); // テストの実施 } finally { setAutomatic(); // 後処理 } } }; } public void setAutomatic() throws UiObjectNotFoundException { if(mYear >= 0 || mMonth >= 0 || mDay >= 0) { _SetAutomatic(); } mYear = -1; mMonth = -1; mDay = -1; } public void setSysDate(int year, int month, int day) throws UiObjectNotFoundException { if(year < MIN_YEAR || year > MAX_YEAR) throw new IllegalArgumentException( String.format("\"%d <= year(%d) <= %d\"", MIN_YEAR, year, MAX_YEAR)); if(year >= 0 && month >= 0 && day >= 0) { _SetSystemDate(year, month, day); } mYear = year; mMonth = month; mDay = day; } ... }
テストルールの使用方法
使用方法はその他のテストルールと変わりません。@Ruleを付けてプロパティ(Javaの場合はフィールド)を宣言するだけです。
テストの途中でもsetSysDateを用いて日付の変更が可能です。
class Rule_SysDate_Test { @get:Rule var dateRule = SysDateTestRule(2019, 0, 1) // ------------------------------------------------------------------------ @Test fun 日付の指定() { val calendar = Calendar.getInstance() Truth.assertThat(calendar[Calendar.YEAR]).isEqualTo(2019) Truth.assertThat(calendar[Calendar.MONTH]).isEqualTo(0) Truth.assertThat(calendar[Calendar.DAY_OF_MONTH]).isEqualTo(1) } @Test fun 指定_未来() { dateRule.setSysDate(2020, 2, 3) // 2019.01.01 -> 2020.03.03 val calendar = Calendar.getInstance() Truth.assertThat(calendar[Calendar.YEAR]).isEqualTo(2020) Truth.assertThat(calendar[Calendar.MONTH]).isEqualTo(2) Truth.assertThat(calendar[Calendar.DAY_OF_MONTH]).isEqualTo(3) } }
public class Rule_SysDate_Test { @Rule public SysDateTestRule mDateRule = new SysDateTestRule(2019, 0, 1); // ------------------------------------------------------------------------ @Test public void A日付の指定() throws Exception { Calendar _Calendar = Calendar.getInstance(); assertThat(_Calendar.get(Calendar.YEAR)).isEqualTo(2019); assertThat(_Calendar.get(Calendar.MONTH)).isEqualTo(0); assertThat(_Calendar.get(Calendar.DAY_OF_MONTH)).isEqualTo(1); } @Test public 日付の指定_未来() throws Exception { mDateRule.setSysDate(2020, 2, 3); // 2019.01.01 -> 2020.03.03 Calendar _Calendar = Calendar.getInstance(); assertThat(_Calendar.get(Calendar.YEAR)).isEqualTo(2020); assertThat(_Calendar.get(Calendar.MONTH)).isEqualTo(2); assertThat(_Calendar.get(Calendar.DAY_OF_MONTH)).isEqualTo(3); } }