UI Automator で自動テストを作成する

UI Automator は、システムアプリとインストール済みアプリにまたがるアプリ間の UI 機能テストに適した UI テスト フレームワークです。UI Automator API を使用すると、どの Activity がフォーカスされているかにかかわらず、デバイス上の表示要素を操作できるため、テストデバイスで設定メニューやアプリ ランチャーを開くなどのオペレーションが可能になります。テストでは、UI コンポーネントやそのコンテンツ説明に表示されるテキストなど、使い勝手のよい記述子を使用して UI コンポーネントを検索できます。

UI Automator テスト フレームワークは、インストルメンテーション ベースの API であり、AndroidJUnitRunner テストランナーと連携して機能します。テストコードがターゲット アプリの細かい内部実装に依存しておらず、不透明なボックス スタイルの自動化テストの作成に適しています。

UI Automator テスト フレームワークの主要な機能は次のとおりです。

  • ターゲット デバイスの状態情報を取得してオペレーションを実行する API。詳細については、デバイスの状態にアクセスするをご覧ください。
  • 複数のアプリにまたがる UI テストをサポートする API。詳細については、UI Automator API をご覧ください。

デバイスの状態にアクセスする

UI Automator テスト フレームワークには、ターゲット アプリが実行されているデバイスにアクセスしてオペレーションを実行するための UiDevice クラスが用意されています。このクラスのメソッドを呼び出すことにより、その時点でのデバイスの向きや表示サイズなどのデバイス プロパティにアクセスできます。UiDevice クラスでは、次のアクションも実行できます。

  1. デバイスの回転を変更する。
  2. 「音量大」などのハードウェア キーを押す。
  3. 「戻る」ボタン、ホームボタン、メニューボタンをクリックする。
  4. 通知シェードを開く。
  5. 現在のウィンドウのスクリーンショットを撮る。

たとえば、ホームボタンの押下をシミュレートするには、UiDevice.pressHome() メソッドを呼び出します。

UI Automator API

UI Automator API を使用すると、ターゲットとするアプリの詳細な実装を把握していなくても、堅牢なテストを作成することが可能になります。以下の API により、複数のアプリにまたがる UI コンポーネントをキャプチャして操作できます。

  • UiObject2: デバイス上に表示される UI 要素を表します。
  • BySelector: UI 要素の照合条件を指定します。
  • By: BySelector を簡潔に構築します。
  • Configurator: UI Automator テストを実行するための主要なパラメータを設定できます。

たとえば、次のコードは、デバイスで Gmail アプリを開くテスト スクリプトの作成方法を示しています。

Kotlin

device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.pressHome()

val gmail: UiObject2 = device.findObject(By.text("Gmail"))
// Perform a click and wait until the app is opened.
val opened: Boolean = gmail.clickAndWait(Until.newWindow(), 3000)
assertThat(opened).isTrue()

Java

device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
device.pressHome();

UiObject2 gmail = device.findObject(By.text("Gmail"));
// Perform a click and wait until the app is opened.
Boolean opened = gmail.clickAndWait(Until.newWindow(), 3000);
assertTrue(opened);

UI Automator をセットアップする

UI Automator を使用して UI テストを作成する前に、AndroidX Test 用にプロジェクトをセットアップするの説明に従って、テスト ソースコードの場所とプロジェクトの依存関係を構成してください。

Android アプリ モジュールの build.gradle ファイルで、UI Automator ライブラリへの依存関係の参照を設定する必要があります。

Kotlin

dependencies {
  ...
  androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
}

Groovy

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

UI Automator テストを最適化するには、最初にターゲット アプリの UI コンポーネントを検査して、それらがアクセス可能であることを確認する必要があります。最適化のヒントについては、次の 2 つのセクションで説明します。

デバイス上の UI を検査する

テストを設計する前に、デバイスに表示される UI コンポーネントを検査します。UI Automator テストでこれらのコンポーネントにアクセスできるようにするには、これらのコンポーネントに表示テキストのラベルと android:contentDescription 値の両方またはいずれかが存在することを確認します。

uiautomatorviewer ツールは、レイアウト階層を検査し、デバイスのフォアグラウンドに表示される UI コンポーネントのプロパティを確認するのに便利な視覚的インターフェースを提供します。この情報により、UI Automator できめ細かいテストを作成することが可能になります。たとえば、特定の表示プロパティに対応する UI セレクタを作成できます。

uiautomatorviewer ツールを起動するには:

  1. 物理デバイスでターゲット アプリを起動します。
  2. デバイスを開発マシンに接続します。
  3. ターミナル ウィンドウを開いて、<android-sdk>/tools/ ディレクトリに移動します。
  4. 次のコマンドでツールを実行します。
 $ uiautomatorviewer

アプリの UI プロパティを表示するには:

  1. uiautomatorviewer インターフェースで、[Device Screenshot] ボタンをクリックします。
  2. 左側のパネルのスナップショットにカーソルを合わせると、uiautomatorviewer ツールによって特定された UI コンポーネントが表示されます。プロパティは右下のパネルにリストされ、レイアウト階層は右上のパネルに表示されます。
  3. 必要に応じて、[Toggle NAF Nodes] ボタンをクリックして、UI Automator からアクセスできない UI コンポーネントを表示できます。こうしたコンポーネントについては、限られた情報しか参照できない可能性があります。

Android が提供する UI コンポーネントの一般的なタイプについては、ユーザー インターフェースをご覧ください。

アクティビティのユーザー補助機能を確認する

UI Automator テスト フレームワークは、Android のユーザー補助機能を実装したアプリのパフォーマンスを向上させます。View 型の UI 要素か、SDK の View のサブクラスを使用する場合、これらのクラスはすでにユーザー補助サポートを実装しているため、改めて実装する必要はありません。

ただし、一部のアプリでは、カスタム UI 要素を使用して、より豊かなユーザー エクスペリエンスを提供できます。そのような要素は、自動的にはユーザー補助サポートを提供しません。SDK 以外の View のサブクラスのインスタンスがアプリに含まれている場合は、次の手順を実施して、それらの要素にユーザー補助機能を追加してください。

  1. ExploreByTouchHelper を拡張する具象クラスを作成します。
  2. setAccessibilityDelegate() を呼び出して、新しいクラスのインスタンスを特定のカスタム UI 要素に関連付けます。

カスタムビュー要素にユーザー補助機能を追加するためのガイダンスについては、アクセス可能なカスタムビューの構築をご覧ください。Android のユーザー補助に関する一般的なおすすめの方法については、アプリのユーザー補助機能を強化するをご覧ください。

UI Automator テストクラスを作成する

UI Automator テストクラスは、JUnit 4 テストクラスと同じ方法で作成する必要があります。JUnit 4 テストクラスの作成方法と、JUnit 4 アサーションおよびアノテーションの使用方法の詳細については、インストゥルメント化単体テストクラスを作成するをご覧ください。

テストクラス定義の先頭に @RunWith(AndroidJUnit4.class) アノテーションを追加します。また、AndroidX Test で提供される AndroidJUnitRunner クラスを、デフォルトのテストランナーとして指定する必要があります。この手順の詳細については、デバイスまたはエミュレータで UI Automator テストを実行するをご覧ください。

UI Automator テストクラスに、次のプログラミング モデルを実装します。

  1. getInstance() メソッドを呼び出し、引数として Instrumentation オブジェクトを渡すことにより、テスト対象のデバイスにアクセスするための UiDevice オブジェクトを取得します。
  2. findObject() メソッドを呼び出して、デバイスに表示される UI コンポーネント(フォアグラウンドの現在のビューなど)にアクセスするための UiObject2 オブジェクトを取得します。
  3. UiObject2 メソッドを呼び出して、その UI コンポーネントで実行する特定のユーザー インタラクションをシミュレートします。たとえば、scrollUntil() を呼び出してスクロールし、setText() を呼び出してテキスト フィールドを編集します。必要に応じてステップ 2 とステップ 3 の API を繰り返し呼び出すことにより、複数の UI コンポーネントまたは一連のユーザー アクションを含む複雑なユーザー インタラクションをテストできます。
  4. ユーザー インタラクションが実行された後、UI に予期される状態または動作が反映されていることを確認します。

上記の手順については、以降のセクションで詳しく説明します。

UI コンポーネントにアクセスする

UiDevice オブジェクトは、デバイスの状態にアクセスして操作するための主要な方法です。テストでは、UiDevice メソッドを呼び出して、現在のデバイスの向きや表示サイズなど、さまざまなプロパティの状態をチェックできます。テストでは、UiDevice オブジェクトを使用して、デバイスレベルのアクション(デバイスに特定の回転をさせる、ハードウェアの D-pad ボタンを押す、ホームボタンやメニューボタンを押すなど)を実行できます。

デバイスのホーム画面からテストを始めることをおすすめします。ホーム画面(またはデバイスで選択したその他のスタート地点)から、UI Automator API が提供するメソッドを呼び出して、特定の UI 要素を選択および操作できます。

次のコード スニペットは、テストで UiDevice のインスタンスを取得して、ホームボタンの押下をシミュレートする方法を示しています。

Kotlin

import org.junit.Before
import androidx.test.runner.AndroidJUnit4
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
...

private const val BASIC_SAMPLE_PACKAGE = "com.example.android.testing.uiautomator.BasicSample"
private const val LAUNCH_TIMEOUT = 5000L
private const val STRING_TO_BE_TYPED = "UiAutomator"

@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = 18)
class ChangeTextBehaviorTest2 {

private lateinit var device: UiDevice

@Before
fun startMainActivityFromHomeScreen() {
  // Initialize UiDevice instance
  device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

  // Start from the home screen
  device.pressHome()

  // Wait for launcher
  val launcherPackage: String = device.launcherPackageName
  assertThat(launcherPackage, notNullValue())
  device.wait(
    Until.hasObject(By.pkg(launcherPackage).depth(0)),
    LAUNCH_TIMEOUT
  )

  // Launch the app
  val context = ApplicationProvider.getApplicationContext<Context>()
  val intent = context.packageManager.getLaunchIntentForPackage(
  BASIC_SAMPLE_PACKAGE).apply {
    // Clear out any previous instances
    addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
  }
  context.startActivity(intent)

  // Wait for the app to appear
  device.wait(
    Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)),
    LAUNCH_TIMEOUT
    )
  }
}

Java

import org.junit.Before;
import androidx.test.runner.AndroidJUnit4;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.Until;
...

@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = 18)
public class ChangeTextBehaviorTest {

  private static final String BASIC_SAMPLE_PACKAGE
  = "com.example.android.testing.uiautomator.BasicSample";
  private static final int LAUNCH_TIMEOUT = 5000;
  private static final String STRING_TO_BE_TYPED = "UiAutomator";
  private UiDevice device;

  @Before
  public void startMainActivityFromHomeScreen() {
    // Initialize UiDevice instance
    device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());

    // Start from the home screen
    device.pressHome();

    // Wait for launcher
    final String launcherPackage = device.getLauncherPackageName();
    assertThat(launcherPackage, notNullValue());
    device.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)),
    LAUNCH_TIMEOUT);

    // Launch the app
    Context context = ApplicationProvider.getApplicationContext();
    final Intent intent = context.getPackageManager()
    .getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE);
    // Clear out any previous instances
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
    context.startActivity(intent);

    // Wait for the app to appear
    device.wait(Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)),
    LAUNCH_TIMEOUT);
    }
}

この例では、@SdkSuppress(minSdkVersion = 18) ステートメントにより、UI Automator フレームワークの要件に従って、Android 4.3(API レベル 18)以上のデバイスでのみテストが実行されるようにしています。

findObject() メソッドを使用して、特定のセレクタ条件に一致するビューを表す UiObject2 を取得します。必要に応じて、アプリテストの他の部分で作成した UiObject2 インスタンスを再利用できます。UI Automator テスト フレームワークは、テストが UiObject2 インスタンスを使用して UI 要素をクリックするかプロパティをクエリするたびに、その時点で一致する表示を検索することにご注意ください。

次のスニペットは、アプリの [Cancel] ボタンと [OK] ボタンを表す UiObject2 インスタンスをテストで構築する方法を示しています。

Kotlin

val okButton: UiObject2 = device.findObject(
    By.text("OK").clazz("android.widget.Button")
)

// Simulate a user-click on the OK button, if found.
if (okButton != null) {
    okButton.click()
}

Java

UiObject2 okButton = device.findObject(
    By.text("OK").clazz("android.widget.Button")
);

// Simulate a user-click on the OK button, if found.
if (okButton != null) {
    okButton.click();
}

セレクタを指定する

アプリの特定の UI コンポーネントにアクセスするには、By クラスを使用して BySelector インスタンスを作成します。BySelector は、表示された UI の特定の要素に対するクエリを表します。

一致する要素が複数見つかった場合、レイアウト階層内で最初に一致した要素がターゲット UiObject2 として返されます。BySelector を構築する際は、複数のプロパティを連結して検索を絞り込むことができます。一致する UI 要素が見つからない場合は、null が返されます。

hasChild() メソッドまたは hasDescendant() メソッドを使用して、複数の BySelector インスタンスをネストできます。たとえば、次のコード例は、テキスト プロパティを持つ子 UI 要素を持つ最初の ListView を検索する方法を示しています。

Kotlin

val listView: UiObject2 = device.findObject(
    By.clazz("android.widget.ListView")
        .hasChild(
            By.text("Apps")
        )
)

Java

UiObject2 listView = device.findObject(
    By.clazz("android.widget.ListView")
        .hasChild(
            By.text("Apps")
        )
);

セレクタの条件としてオブジェクトの状態を指定する方法が適切な場合もあります。たとえば、すべてのチェック済み要素のリストを選択してチェックを解除するには、引数を true に設定して checked() メソッドを呼び出します。

アクションを実行する

テストで UiObject2 オブジェクトを取得したら、UiObject2 クラスのメソッドを呼び出して、そのオブジェクトが表す UI コンポーネントでユーザー インタラクションを実行できます。次のようなアクションの指定が可能です。

  • click() : UI 要素の表示境界の中心をクリックします。
  • drag() : このオブジェクトを任意の座標にドラッグします。
  • setText() : 編集可能フィールドの内容をクリアした後、フィールドにテキストを設定します。一方、clear() メソッドは、編集可能フィールドの既存のテキストをクリアします。
  • swipe() : 指定された方向にスワイプ アクションを実行します。
  • scrollUntil(): Condition または EventCondition が満たされるまで、指定された方向にスクロール アクションを実行します。

UI Automator テスト フレームワークでは、getContext()Context オブジェクトを取得することにより、シェルコマンドを使用せずに Intent を送信したり Activity を起動したりすることができます。

次のスニペットは、テストで Intent を使用してテスト対象のアプリを起動する方法を示しています。このアプローチは、電卓アプリのテストのみが重要で、アプリを起動するランチャーは無視してよい場合に役立ちます。

Kotlin

fun setUp() {
...

  // Launch a simple calculator app
  val context = getInstrumentation().context
  val intent = context.packageManager.getLaunchIntentForPackage(CALC_PACKAGE).apply {
    addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
  }
  // Clear out any previous instances
  context.startActivity(intent)
  device.wait(Until.hasObject(By.pkg(CALC_PACKAGE).depth(0)), TIMEOUT)
}

Java

public void setUp() {
...

  // Launch a simple calculator app
  Context context = getInstrumentation().getContext();
  Intent intent = context.getPackageManager()
  .getLaunchIntentForPackage(CALC_PACKAGE);
  intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);

  // Clear out any previous instances
  context.startActivity(intent);
  device.wait(Until.hasObject(By.pkg(CALC_PACKAGE).depth(0)), TIMEOUT);
}

結果を検証する

InstrumentationTestCaseTestCase を拡張しているため、標準の JUnit Assert メソッドを使用して、アプリ内の UI コンポーネントが期待される結果を返すかどうかをテストできます。

次のスニペットは、テストで電卓アプリのボタンの位置を特定して順にクリックし、正しい結果が表示されることを確認する方法を示しています。

Kotlin

private const val CALC_PACKAGE = "com.myexample.calc"

fun testTwoPlusThreeEqualsFive() {
  // Enter an equation: 2 + 3 = ?
  device.findObject(By.res(CALC_PACKAGE, "two")).click()
  device.findObject(By.res(CALC_PACKAGE, "plus")).click()
  device.findObject(By.res(CALC_PACKAGE, "three")).click()
  device.findObject(By.res(CALC_PACKAGE, "equals")).click()

  // Verify the result = 5
  val result: UiObject2 = device.findObject(By.res(CALC_PACKAGE, "result"))
  assertEquals("5", result.text)
}

Java

private static final String CALC_PACKAGE = "com.myexample.calc";

public void testTwoPlusThreeEqualsFive() {
  // Enter an equation: 2 + 3 = ?
  device.findObject(By.res(CALC_PACKAGE, "two")).click();
  device.findObject(By.res(CALC_PACKAGE, "plus")).click();
  device.findObject(By.res(CALC_PACKAGE, "three")).click();
  device.findObject(By.res(CALC_PACKAGE, "equals")).click();

  // Verify the result = 5
  UiObject2 result = device.findObject(By.res(CALC_PACKAGE, "result"));
  assertEquals("5", result.getText());
}

デバイスまたはエミュレータで UI Automator テストを実行する

UI Automator テストは、Android Studio またはコマンドラインから実行できます。プロジェクトで、デフォルトのインストゥルメンテーション ランナーとして AndroidJUnitRunner を指定してください。

その他の例

システム UI を操作する

次のコード スニペットに示すように、UI Automator は、アプリ外のシステム要素など、画面上のすべての要素を操作できます。

Kotlin

// Opens the System Settings.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.executeShellCommand("am start -a android.settings.SETTINGS")

Java

// Opens the System Settings.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
device.executeShellCommand("am start -a android.settings.SETTINGS");

Kotlin

// Opens the notification shade.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.openNotification()

Java

// Opens the notification shade.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
device.openNotification();

Kotlin

// Opens the Quick Settings shade.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.openQuickSettings()

Java

// Opens the Quick Settings shade.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
device.openQuickSettings();

Kotlin

// Get the system clock.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
UiObject2 clock = device.findObject(By.res("com.android.systemui:id/clock"))
print(clock.getText())

Java

// Get the system clock.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
UiObject2 clock = device.findObject(By.res("com.android.systemui:id/clock"));
print(clock.getText());

遷移を待つ

サイレント モードをオフにする
図 1. UI Automator がテストデバイスでサイレント モードをオフにします。

画面遷移には時間がかかり、その所要時間を予測することはできません。そのため、UI Automator は操作の実行後に待機する必要があります。UI Automator には、このための複数の方法が用意されています。

次のコード スニペットは、UI Automator を使用して、遷移を待機する performActionAndWait() メソッドを使用して、システム設定でサイレント モードをオフにする方法を示しています。

Kotlin

@Test
@SdkSuppress(minSdkVersion = 21)
@Throws(Exception::class)
fun turnOffDoNotDisturb() {
    device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    device.performActionAndWait({
        try {
            device.executeShellCommand("am start -a android.settings.SETTINGS")
        } catch (e: IOException) {
            throw RuntimeException(e)
        }
    }, Until.newWindow(), 1000)
    // Check system settings has been opened.
    Assert.assertTrue(device.hasObject(By.pkg("com.android.settings")))

    // Scroll the settings to the top and find Notifications button
    var scrollableObj: UiObject2 = device.findObject(By.scrollable(true))
    scrollableObj.scrollUntil(Direction.UP, Until.scrollFinished(Direction.UP))
    val notificationsButton = scrollableObj.findObject(By.text("Notifications"))

    // Click the Notifications button and wait until a new window is opened.
    device.performActionAndWait({ notificationsButton.click() }, Until.newWindow(), 1000)
    scrollableObj = device.findObject(By.scrollable(true))
    // Scroll down until it finds a Do Not Disturb button.
    val doNotDisturb = scrollableObj.scrollUntil(
        Direction.DOWN,
        Until.findObject(By.textContains("Do Not Disturb"))
    )
    device.performActionAndWait({ doNotDisturb.click() }, Until.newWindow(), 1000)
    // Turn off the Do Not Disturb.
    val turnOnDoNotDisturb = device.findObject(By.text("Turn on now"))
    turnOnDoNotDisturb?.click()
    Assert.assertTrue(device.wait(Until.hasObject(By.text("Turn off now")), 1000))
}

Java

@Test
@SdkSuppress(minSdkVersion = 21)
public void turnOffDoNotDisturb() throws Exception{
    device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
    device.performActionAndWait(() -> {
        try {
            device.executeShellCommand("am start -a android.settings.SETTINGS");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }, Until.newWindow(), 1000);
    // Check system settings has been opened.
    assertTrue(device.hasObject(By.pkg("com.android.settings")));

    // Scroll the settings to the top and find Notifications button
    UiObject2 scrollableObj = device.findObject(By.scrollable(true));
    scrollableObj.scrollUntil(Direction.UP, Until.scrollFinished(Direction.UP));
    UiObject2 notificationsButton = scrollableObj.findObject(By.text("Notifications"));

    // Click the Notifications button and wait until a new window is opened.
    device.performActionAndWait(() -> notificationsButton.click(), Until.newWindow(), 1000);
    scrollableObj = device.findObject(By.scrollable(true));
    // Scroll down until it finds a Do Not Disturb button.
    UiObject2 doNotDisturb = scrollableObj.scrollUntil(Direction.DOWN,
            Until.findObject(By.textContains("Do Not Disturb")));
    device.performActionAndWait(()-> doNotDisturb.click(), Until.newWindow(), 1000);
    // Turn off the Do Not Disturb.
    UiObject2 turnOnDoNotDisturb = device.findObject(By.text("Turn on now"));
    if(turnOnDoNotDisturb != null) {
        turnOnDoNotDisturb.click();
    }
    assertTrue(device.wait(Until.hasObject(By.text("Turn off now")), 1000));
}

参考情報

Android テストで UI Automator を使用する方法の詳細については、次のリソースをご覧ください。

リファレンス ドキュメント:

サンプル

  • BasicSample: UI Automator の基本的なサンプル。