Android端末の時刻を変更するJUnitテストルール

投稿日:  更新日:

スケジュール管理やアラーム機能を実装するアプリのテストで、端末の時刻を自由に変更できたら便利です。なので、端末の時刻を変更するテストルールを作成してみました。

この記事は「Android端末の時刻を変更するJUnitテストルール(Api23~34対応)」で改訂されました。

スポンサーリンク

時刻を変更する方法

dateツールを使う

adb shellコマンドのdateツールを使って変更します。

使い方は次の通りです。[SET]があれば設定し、無ければ現在時刻を表示します。

# date -h
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分に変更してみましょう。

# date 12312359
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の操作で変更します。つまり、ユーザが手で操作するのと同じことをプログラム的に行わせようというわけです。

KotlinJava
    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<29API≧29
settingsアプリの日付時刻設定(API<29)Settingsアプリの日付時刻設定(API≧29)

時刻の設定はダイアル式になっています。ドラッグした位置からスクリーン上をスライドさせることで細かな指定ができます。でも、プログラム中から行うのが困難だったのでダイアルの数字を検出してクリックしています。

時設定のダイアル分設定のダイアル
Settingsアプリの時設定のダイアルSettingsアプリの分設定のダイアル

ただ、この方法には注意点があります。

  • GUI操作の動作が遅い
  • 操作方法はAVDが基準、カスタマイズされていると動かない
  • API==21,22の場合、UiDevice#findObjectでObjectが見つけられない(原因不明)
スポンサーリンク

Automatic(Networkから取得)へ変更する方法

SettingsアプリをGUI操作

Settingsアプリを開いてGUIの操作で変更します。「時刻を変更する方法」と同様です。

Automaticへ変更すると直ちにNetworkから時刻を取得してくれます。

KotlinJava
    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へ変更する
KotlinJava
@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を用いて時刻の変更が可能です。

KotlinJava
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);
    }
}
スポンサーリンク
スポンサーリンク