UiAutomatorを使ってAndroidのUIテストを行う方法を紹介します。
目次
UiAutomatorとは
UiAutomatorはAndroidのUIをテストするために開発されたフレームワークです。Android公認のフレームワークになっています。
UiAutomatorを使うことで実行中のアプリのUI要素(View)を検出し、UIの操作(クリック、スワイプ、など)や、UIの状態(ボタンの表示、文字の出力、など)チェックを行うテストプログラムが記述できるようになります。
同じようなフレームワークにEspressoがあります。この2つはテスト対象の範囲に違いがあります。
UiAutomatorはブラックボックス
UiAutomatorのテスト対象はアンドロイド端末にインストールされている全てのアプリです。
複数のアプリを渡り歩いてテストができるので、アプリ間の連携を確認できます。例えば、開発中のカメラアプリで撮影した画像がフォトビューワーアプリで表示できることを確認するといった事です。
UiAutomatorはアプリをテストする時にアプリの内部構造を知りません。実行されているアプリの、画面に表示されているままをテストします。
このように、アプリの内部構造を知らない状態でテストすることを「ブラックボックステスト」といいます。
また、UiAutomatorはデバイスの状態をエミュレートできます。状態とは端末の向きやホームボタンの押下などで発生した事象です。デバイスへ物理的な操作を加えていませんが、あたかも加えて状態が変化したかのようにアプリへ伝えます。
不便な点として、Espressoと違いMainスレッドと同期が取れないので注意が必要です。なので、EspressoとUiAutomatorを併用すると、待ち合わせが必要になる場合があります。
さらに詳しい情報は「UI Automator」「複数のアプリのUIをテストする」を参照してください。
Espressoはホワイトボックス
Espressoのテスト対象はアプリ内の全てのActivityです。
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は次のように取得できます。
private val uiDevice = UiDevice.getInstance( InstrumentationRegistry.getInstrumentation())
private UiDevice mUiDevice = UiDevice.getInstance( InstrumentationRegistry.getInstrumentation());
テストを行う
UI要素の操作(検出/同期/操作/検証)
UI要素(View)はObject2クラスで表現されます。Object2は画面に表示中のUI要素を検出して得られます。検出後はObject2を使ってUI要素を操作する事が出来ます。
// --- 検出 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要素はチェックされています)
デバイスの状態
デバイスの状態は物理的な操作をすることなく状態をエミュレートします。エミュレートされた状態がアプリに伝わるので、伝わった状態に応答してアプリは動きを変えます。
// --- ボタン 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です。
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)が返ります。
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 <-- 起動
アプリの解析
解析対象のアプリを起動した後、「Device Screenshot」アイコン(左から2つ目)をクリックすると、解析が行われます。
解析結果は3つのペインを持ちます。
左がアプリの画面をキャプチャした画像です。右上がレイアウト階層です。右下がUI要素のプロパティです。
表示されているUI要素が赤枠で囲まれたり、ハイライトされたりします。