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要素が赤枠で囲まれたり、ハイライトされたりします。
