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




