大規模テストの安定性

モバイルアプリやフレームワークは非同期の性質を持つため、信頼性が高く再現性のあるテストの作成が困難になることがよくあります。ユーザー イベントが注入されると、テスト フレームワークは、アプリがそれに対する反応(画面上のテキストの変更からアクティビティの完全な再作成まで)を完了するまで待機する必要があります。テストの動作が確定的でない場合は、不安定です。

Compose や Espresso などの最新のフレームワークはテストを念頭に設計されているため、次のテスト アクションまたはアサーションの前に UI がアイドル状態になることが保証されています。これは同期です。

同期をテストする

データベースからのデータの読み込みや無限アニメーションの表示など、テストで不明な非同期オペレーションやバックグラウンド オペレーションを実行すると、問題が発生する可能性があります。

テストパスを実行する前にアプリがアイドル状態かどうかを確認するループを示すフロー図
図 1: テストの同期。

テストスイートの信頼性を高めるには、Espresso アイドリング リソースなど、バックグラウンド オペレーションを追跡する方法を実装します。また、コルーチン用の TestDispatcher や RxJava 用の RxIdler など、アイドル状態をクエリできるバージョンをテストしたり、同期を改善したりするテスト バージョン用のモジュールを置き換えることもできます。

同期が一定時間の待機に基づいている場合のテスト失敗を示す図
図 2: テストで sleep を使用すると、テストが遅くなったり不安定になったりします。

安定性を高める方法

大規模なテストはアプリの複数のコンポーネントをテストするため、多くの回帰を同時に検出できます。通常はエミュレータまたはデバイスで実行されるため、忠実度が高くなります。大規模なエンドツーエンド テストは包括的なカバレッジを提供しますが、時折失敗する傾向があります。

不安定性を軽減するために講じることができる主な対策は次のとおりです。

  • デバイスを正しく設定する
  • 同期の問題を防ぐ
  • 再試行を実装する

Compose または Espresso を使用して大規模なテストを作成するには、通常、いずれかのアクティビティを開始し、ユーザーが操作するように移動し、アサーションまたはスクリーンショット テストを使用して UI が正しく動作することを確認します。

UI Automator などの他のフレームワークでは、システム UI や他のアプリを操作できるため、より広範なスコープを許可できます。ただし、UI Automator テストでは手動での同期が必要になるため、信頼性が低くなる傾向があります。

デバイスを設定する

まず、テストの信頼性を高めるには、デバイスのオペレーティング システムがテストの実行を予期せず中断しないようにする必要があります。たとえば、システム アップデート ダイアログが他のアプリの上に重ねて表示されている場合や、ディスク上の容量が不足している場合などです。

デバイスファーム プロバイダがデバイスとエミュレータを設定するため、通常は操作する必要はありません。ただし、特殊なケース用に独自の構成ディレクティブが存在する場合があります。

Gradle で管理されているデバイス

エミュレータを自分で管理する場合は、Gradle で管理されているデバイスを使用して、テストの実行に使用するデバイスを定義できます。

android {
  testOptions {
    managedDevices {
      localDevices {
        create("pixel2api30") {
          // Use device profiles you typically see in Android Studio.
          device = "Pixel 2"
          // Use only API levels 27 and higher.
          apiLevel = 30
          // To include Google services, use "google".
          systemImageSource = "aosp"
        }
      }
    }
  }
}

この構成では、次のコマンドでエミュレータ イメージを作成し、インスタンスを起動してテストを実行し、シャットダウンします。

./gradlew pixel2api30DebugAndroidTest

Gradle で管理されているデバイスには、デバイスの接続解除が発生した場合に再試行するメカニズムやその他の改善が含まれています。

同期の問題を防止する

バックグラウンド オペレーションまたは非同期オペレーションを行うコンポーネントでは、UI の準備が整う前にテスト ステートメントが実行されるため、テストが失敗する可能性があります。テストの範囲が広くなるほど、不安定になる可能性が高くなります。テスト フレームワークは、アクティビティの読み込みが完了したのか、もう少し待つ必要があるのかを推測する必要があるため、これらの同期の問題が不安定性の主な原因となります。

ソリューション

Espresso のアイドリング リソースを使用すると、アプリがビジー状態になるタイミングを通知できますが、すべての非同期オペレーションを追跡するのは困難です。大規模なエンドツーエンド テストでは特にそうです。また、アイドル状態のリソースをインストールすると、テスト対象のコードが汚染される可能性があります。

アクティビティがビジーかどうかを推定する代わりに、特定の条件が満たされるまでテストを待機させることができます。たとえば、特定のテキストやコンポーネントが UI に表示されるまで待機できます。

Compose には、さまざまなマッチャーを待機するためのテスト API のコレクションが ComposeTestRule の一部として用意されています。

fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeout: Long = 1000L)

fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeout: Long = 1000L)

fun waitUntilExactlyOneExists(matcher: SemanticsMatcher,  timeout: Long = 1000L)

fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeout: Long = 1000L)

ブール値を返す任意の関数を受け取る汎用 API もあります。

fun waitUntil(timeoutMillis: Long, condition: () -> Boolean): Unit

使用例:

composeTestRule.waitUntilExactlyOneExists(hasText("Continue")</code>)</p></td>

再試行メカニズム

不安定なテストは修正する必要がありますが、失敗する条件が非常にありそうもないため、再現が難しい場合があります。不安定なテストは常に追跡して修正する必要がありますが、再試行メカニズムを使用すると、テストが成功するまで何度もテストを実行することで、デベロッパーの生産性を維持できます。

次のような問題を防ぐには、複数のレベルで再試行する必要があります。

  • デバイスへの接続がタイムアウトした、または接続が切断された
  • 単一のテスト失敗

再試行のインストールまたは構成は、テスト フレームワークとインフラストラクチャによって異なりますが、一般的なメカニズムには次のようなものがあります。

  • テストを数回再試行する JUnit ルール
  • CI ワークフローの再試行アクションまたはステップ
  • Gradle で管理されているデバイスなど、エミュレータが応答しなくなったときにエミュレータを再起動するシステム。