UiAutomatorでAndroidのUIテストを行う

投稿日:  更新日:

UiAutomatorを使ってAndroidのUIテストを行う方法を紹介します。

スポンサーリンク

UiAutomatorとは

UiAutomatorはAndroidのUIをテストするために開発されたフレームワークです。Android公認のフレームワークになっています。

UiAutomatorを使うことで実行中のアプリのUI要素(View)を検出し、UIの操作(クリック、スワイプ、など)や、UIの状態(ボタンの表示、文字の出力、など)チェックを行うテストプログラムが記述できるようになります。

同じようなフレームワークにEspressoがあります。この2つはテスト対象の範囲に違いがあります。

UiAutomatorはブラックボックス

UiAutomatorのテスト対象はアンドロイド端末にインストールされている全てのアプリです。

複数のアプリを渡り歩いてテストができるので、アプリ間の連携を確認できます。例えば、開発中のカメラアプリで撮影した画像がフォトビューワーアプリで表示できることを確認するといった事です。

UiAutomatorのテスト対象

UiAutomatorはアプリをテストする時にアプリの内部構造を知りません。実行されているアプリの、画面に表示されているままをテストします。

このように、アプリの内部構造を知らない状態でテストすることを「ブラックボックステスト」といいます。

また、UiAutomatorはデバイスの状態をエミュレートできます。状態とは端末の向きやホームボタンの押下などで発生した事象です。デバイスへ物理的な操作を加えていませんが、あたかも加えて状態が変化したかのようにアプリへ伝えます。

不便な点として、Espressoと違いMainスレッドと同期が取れないので注意が必要です。なので、EspressoとUiAutomatorを併用すると、待ち合わせが必要になる場合があります。

さらに詳しい情報は「UI Automator」「複数のアプリのUIをテストする」を参照してください。

Espressoはホワイトボックス

Espressoのテスト対象はアプリ内の全てのActivityです。

Espressoのテスト対象

Espressoはアプリをテストする時にプログラム中でActivityを起動します。従って、アプリを構成するActivityを知っている必要があります。また、Activityを構成するViewを検出してUIの操作や状態チェックを行います。従って、Activityを構成するViewを知っている必要があります。

このように、内部構造を知っている状態でテストすることを「ホワイトボックステスト」といいます。

さらに詳しい情報は「EspressoでAndroidのUIテストを行う」を参照してください。

スポンサーリンク

環境設定

UiAutomatorを使うためにライブラリの依存リストへ次の一行を追加します。

dependencies {
    ...
    androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
    ...
}

UiAutomatorはAnrdoidのUIテストを行うフレームワークです。Android上でテストを動かす必要がありますので、テストはインストゥルメントになります。

スポンサーリンク

UiDeviceの取得

UiAutomatorの準備として初めにUiDeviceのオブジェクトを取得します。

UiDeviceは先の図に示した通り、テストとアプリまたはデバイス間のインターフェイスのような役割を果たします。テストプログラムからUiDeviceを介してアプリとデバイスへアクセスします。

UiDeviceは次のように取得できます。

KotlinJava
    private val uiDevice = UiDevice.getInstance(
        InstrumentationRegistry.getInstrumentation())
    private UiDevice mUiDevice = UiDevice.getInstance(
            InstrumentationRegistry.getInstrumentation());
スポンサーリンク

テストを行う

UI要素の操作(検出/同期/操作/検証)

UI要素(View)はObject2クラスで表現されます。Object2は画面に表示中のUI要素を検出して得られます。検出後はObject2を使ってUI要素を操作する事が出来ます。

KotlinJava
        // --- 検出
        val _uiObjectA = uiDevice.findObject(
            By.text("OK").clazz(Button::class.java))
        // ↑ "OK"と表示されているButtonを検出する
        val _uiObjectB = uiDevice.findObject(
            By.res("com.example.sample:id/btnNG"))
        // ↑ 指定したパッケージとリソースIDを持つViewを検出する
        val _uiObjectC = uiDevice.findObject(
            By.res("com.example.sample", "txtSample"))
        // ↑ 指定したパッケージとリソースIDを持つViewを検出する
        val _uiObjectE = uiDevice.findObject(
            By.clazz("android.widget.EditText "))
        // ↑ 指定したクラス名のView(EditText)を検出する

        // --- 検出(コンディション付き)
        val _uiObjectD = uiDevice.wait(Until.findObject(
                By.text("Sample")), 1000)
        // ↑ "Hoge"と表示されているViewを検出する、1[秒]以内の制限付き
        // ↑ 検出できなければNullが返る

        // --- 同期
        uiDevice.pressHome()
        val _resultA = uiDevice.wait(Until.hasObject(
                By.pkg(uiDevice.launcherPackageName)), 2000)
        assertThat(_resultA).isTrue()   // Truthを使用
        // ↑ ランチャーが出現するのを2[秒]間待つ
        // ↑ 出現しなければfalseが返る
        val _resultB = _uiObjectA.clickAndWait(Until.newWindow(), 3000)
        assertThat(_resultB).isTrue()   // Truthを使用
        // ↑ Viewをクリックしてダイアログが出現するのを3[秒]間待つ
        // ↑ 出現しなければfalseが返る

        // --- 操作
        _uiObjectA.click()          // クリックする
        _uiObjectB.longClick()      // ロングクリックする
        _uiObjectE.text = "Hoge"    // "Hoge"をセットする

        // --- 検証
        assertThat(_uiObjectE.text).isEqualTo("Hoge")    // Truthを使用
        // ↑ UI要素のテキストは"Hoge"と等しい
        assertThat(_uiObjectC.isChecked).isEqualTo(true) // Truthを使用
        // ↑ UI要素のチェック状態はtrueです(UI要素はチェックされています)
        // --- 検出
        UiObject2 _UiObjectA = mUiDevice.findObject(
                By.text("OK").clazz(Button.class));
        // ↑ "OK"と表示されているButtonを検出する
        UiObject2 _UiObjectB = mUiDevice.findObject(
                By.res("com.example.sample:id/btnNG"));
        // ↑ 指定したパッケージとリソースIDを持つViewを検出する
        UiObject2 _UiObjectC = mUiDevice.findObject(
                By.res("com.example.sample", "txtSample"));
        // ↑ 指定したパッケージとリソースIDを持つViewを検出する
        UiObject2 _UiObjectE = mUiDevice.findObject(
                By.clazz("android.widget.EditText "));
        // ↑ 指定したクラス名のView(EditText)を検出する

        // --- 検出(コンディション付き)
        UiObject2 _UiObjectD = mUiDevice.wait(Until.findObject(
                By.text("Sample")), 1000);
        // ↑ "Hoge"と表示されているViewを検出する、1[秒]以内の制限付き
        // ↑ 検出できなければNullが返る

        // --- 同期
        mUiDevice.pressHome();
        Boolean _ResultA = mUiDevice.wait(Until.hasObject(
                By.pkg(mUiDevice.getLauncherPackageName())), 2000);
        assertThat(_ResultA).isTrue();	// Truthを使用
        // ↑ ランチャーが出現するのを2[秒]間待つ
        // ↑ 出現しなければfalseが返る
        Boolean _ResultB = _UiObjectA.clickAndWait(Until.newWindow(), 3000);
        assertThat(_ResultB).isTrue();	// Truthを使用
        // ↑ Viewをクリックしてダイアログが出現するのを3[秒]間待つ
        // ↑ 出現しなければfalseが返る

        // --- 操作
        _UiObjectA.click();          // クリックする
        _UiObjectB.longClick();      // ロングクリックする
        _UiObjectE.setText("Hoge");  // "Hoge"をセットする

        // --- 検証
        assertThat(_UiObjectE.getText()).isEqualTo("Hoge");  // Truthを使用
        // ↑ UI要素のテキストは"Hoge"と等しい
        assertThat(_UiObjectC.isChecked()).isEqualTo(true);  // Truthを使用
        // ↑ UI要素のチェック状態はtrueです(UI要素はチェックされています)

デバイスの状態

デバイスの状態は物理的な操作をすることなく状態をエミュレートします。エミュレートされた状態がアプリに伝わるので、伝わった状態に応答してアプリは動きを変えます。

KotlinJava
        // --- ボタン
        uiDevice.pressBack() 	// BACKボタンを押す
        uiDevice.pressHome() 	// HOMEボタンを押す
        uiDevice.pressMenu() 	// MENUボタンを押す
        uiDevice.sleep() 		// 電源ボタンを押す
        // ↑ スクリーンONならばOFFにする、OFFならばONにする
        uiDevice.wakeUp() 		// 電源ボタンを押す
        // ↑ スクリーンONならば何もしない、OFFならばONにする

        // --- 端末の向き
        uiDevice.setOrientationNatural()	// 端末を標準の向きにする
        uiDevice.setOrientationLeft() 		// 端末を左へ90度回す
        uiDevice.setOrientationRight() 		// 端末を右へ90度回す

        // --- 画面のタッチ
        uiDevice.click(10, 10) 	// 座標(10,10)の位置をクリック
        uiDevice.swipe(10, 10, 200, 200, 100)
        // ↑ 座標(10,10)~(200,100)間を0.5[秒]でスワイプ
        //   第5引数はstep、100[step]は0.5[秒]、200[step]は1.0[秒]
        uiDevice.drag(10, 10, 200, 200, 100)
        // ↑ 座標(10,10)~(200,100)間を0.5[秒]でドラッグ
        //   第5引数はstep、100[step]は0.5[秒]、200[step]は1.0[秒]
        // --- ボタン
        mUiDevice.pressBack();  // BACKボタンを押す
        mUiDevice.pressHome();  // HOMEボタンを押す
        mUiDevice.pressMenu();  // MENUボタンを押す
        mUiDevice.sleep();      // 電源ボタンを押す
        // ↑ スクリーンONならばOFFにする、OFFならばONにする
        mUiDevice.wakeUp();     // 電源ボタンを押す
        // ↑ スクリーンONならば何もしない、OFFならばONにする

        // --- 端末の向き
        mUiDevice.setOrientationNatural();  // 端末を標準の向きにする
        mUiDevice.setOrientationLeft();     // 端末を左へ90度回す
        mUiDevice.setOrientationRight();    // 端末を右へ90度回す

        // --- 画面のタッチ
        mUiDevice.click(10,10);    // 座標(10,10)の位置をクリック
        mUiDevice.swipe(10,10,200,200,100);
        // ↑ 座標(10,10)~(200,100)間を0.5[秒]でスワイプ
        //   第5引数はstep、100[step]は0.5[秒]、200[step]は1.0[秒]
        mUiDevice.drag(10,10,200,200,100);
        // ↑ 座標(10,10)~(200,100)間を0.5[秒]でドラッグ
        //   第5引数はstep、100[step]は0.5[秒]、200[step]は1.0[秒]

スクリーンショット

画面のスクリーンショットが撮影できます。撮影画像はPNGフォーマットでファイルへ記録されます。デフォルトはスケーリング:x1.0、Quality:90です。

KotlinJava
        val _folder = TemporaryFolder()
        _folder.create()
        val _file = File(_folder.root, "Sample.png")

        uiDevice.takeScreenshot(_file) // スクリーンショットを撮る
        uiDevice.takeScreenshot(_file, 1.0f, 90) // スケール1.0、Quality90
        TemporaryFolder _Folder = new TemporaryFolder();
        _Folder.create();
        File _File = new File(_Folder.getRoot(), "Sample.png");

        mUiDevice.takeScreenshot(_File);           // スクリーンショットを撮る
        mUiDevice.takeScreenshot(_File, 1.0f, 90); // スケール1.0、Quality90

シェルコマンドの実行

シェルコマンドが実行できます。シェルコマンドの標準出力(STDOUT)が返ります。

KotlinJava
        val _stdOut = uiDevice.executeShellCommand("whoami")
        // ↑ Shellコマンドを実行する、戻り値は標準出力
        String _StdOut = mUiDevice.executeShellCommand("whoami");
        // ↑ Shellコマンドを実行する、戻り値は標準出力

※私が実行する限り、パイプ(|)やリダイレクト(>)を含むと動作しません。単一のコマンドのみのようです。

UiAutomatorViewer

UiAutomatorViewerとは

UiAutomatorViewerはアプリのレイアウト階層とUI要素(View)のプロパティ(クラス名、リソースID、テキスト、など)を解析してくれる便利ツールです。

Android端末にインストールされていて、動作可能なアプリであれば、どれでも解析が可能です。

Android端末にプリインストールされているアプリや、GooglePlayからインストールしたアプリをUiAutomatorでテストしたい場合に、表示されているUI要素のプロパティが必要になります。そのような時、UiAutomatorViewerで解析して調べることができます。

起動方法

ツールはAndroid SDKのインストール先にあります。

Android StudioのTerminalを開き、下記のように起動します。

Z:\SDKのインストール先\tools\bin> dir
 ドライブ G のボリューム ラベルは Tools です
 ボリューム シリアル番号は B8BD-BC6E です

 Z:\SDKのインストール先\tools\bin のディレクトリ

2017/11/24  10:15    <DIR>          .
2017/11/24  10:15    <DIR>          ..
2017/09/21  08:36             6,742 apkanalyzer
2017/09/21  08:36             2,227 archquery.bat
2017/09/21  08:36             3,035 avdmanager.bat
2017/09/21  08:36             2,215 jobb.bat
2017/09/21  08:36             3,839 lint.bat
2017/09/21  08:36             2,053 monkeyrunner.bat
2017/09/21  08:36             3,042 sdkmanager.bat
2017/09/21  08:36             2,189 uiautomatorviewer.bat <-- ## これ ##

Z:\SDKのインストール先\tools\bin> uiautomatorviewer     <-- 起動

UiAutomatorViewerを起動したところ

アプリの解析

解析対象のアプリを起動した後、「Device Screenshot」アイコン(左から2つ目)をクリックすると、解析が行われます。

UiAutomatorViewerで解析したところ

解析結果は3つのペインを持ちます。

左がアプリの画面をキャプチャした画像です。右上がレイアウト階層です。右下がUI要素のプロパティです。

表示されているUI要素が赤枠で囲まれたり、ハイライトされたりします。

スポンサーリンク
スポンサーリンク