スケジュール管理やアラーム機能を実装するアプリのテストで、端末の時刻を自由に変更できたら便利です。なので、端末の時刻を変更するテストルールを作成してみました。
この記事は「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);
}
}




