Android端末の日付を変更するJUnitテストルール

投稿日:  更新日:

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

スポンサーリンク

日付を変更する方法

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 _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);
    }
※上記のコードはLanguage⇒Englishのみ対応

操作はAVD(Android Virtual Device)上で行う場合を基準にしています。

API≧29で項目名が変更になっているため、操作対象のObjectを見つける時のテキストをプログラム中で切り替える必要がありました。

API<29API≧29
settingsアプリの日付時刻設定(API<29)Settingsアプリの日付時刻設定(API≧29)

日付の選択はカレンダー式になっています。日付をクリックすることで指定できます。

日付設定のカレンダー(API<23)日付設定のカレンダー(API≧23)
ただ、この方法には注意点があります。

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

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