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

UI Automator は、システムとインストール済みのアプリにまたがるアプリ間の UI 機能テストに適した UI テスト フレームワークです。UI Automator API を使用すると、フォーカスされている Activity に関係なく、デバイス上の表示要素を操作できるため、テストデバイスで設定メニューやアプリ ランチャーを開くなどの操作を実行できます。テストでは、コンポーネントに表示されるテキストやコンテンツの説明などの便利な記述子を使用して、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-alpha03')
}

Groovy

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

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() メソッドを呼び出して、UiObject2 オブジェクトを取得し、デバイスに表示されている UI コンポーネント(フォアグラウンドの現在のビューなど)にアクセスします。
  3. UiObject2 メソッドを呼び出して、その UI コンポーネントで実行する特定のユーザー操作をシミュレートします。たとえば、スクロールするには scroll until() を呼び出します。テキスト フィールドを編集する場合は 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 を使用してテスト対象のアプリを起動する方法を示しています。この方法は、電卓アプリのテストにのみ関心があり、ランチャーは対象外の場合に有用です。

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 の基本的なサンプル。