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 インスタンスをネストできます。たとえば次のコード例は、テストで検索を指定して、text プロパティを持つ子 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 の基本的なサンプル。