Android アプリのテストの基礎

このページでは、Android アプリのテストにおける基本となるベスト プラクティスとその利点などについて概説します。

テストのメリット

アプリの開発において、テストは非常に重要なプロセスです。アプリに対して一貫性のあるテストを実施することで、アプリの公開前に、その正確性、機能の動作、使いやすさを検証できます。

アプリを操作して手動でテストできます。さまざまなデバイスやエミュレータを使用したり、システム言語を変更したり、すべてのユーザーエラーを生成したり、すべてのユーザーフローを走査したりすることがあります。

しかし、手動テストではスケーリングが難しく、アプリの動作の回帰を見落としがちです。自動テストでは、自動テストを行うツールを使用します。このツールは、より高速で再現性が高く、一般的に、開発プロセスの早い段階でアプリに関する実用的なフィードバックを提供できます。

Android でのテストの種類

モバイルアプリは複雑で、多くの環境で適切に動作する必要があります。そのため、テストには多くの種類があります。

対象

たとえば、サブジェクトに応じて異なる種類のテストがあります。

  • 機能テスト: アプリは想定されたとおりに動作しているか?
  • パフォーマンス テスト: 迅速かつ効率的に実行するか。
  • ユーザー補助機能のテスト: ユーザー補助サービスと連動するか。
  • 互換性テスト: すべてのデバイスと API レベルで適切に動作するか。

サポート範囲

また、テストはサイズまたは分離の度合いによって異なります。

  • 単体テストや小規模テストでは、メソッドやクラスなど、アプリのごく一部を検証します。
  • エンドツーエンド テストや大規模なテストでは、画面全体やユーザーフローなど、アプリの大きな部分を同時に検証します。
  • 中テストが中間にあり、2 つ以上のユニットの統合を確認します。
テストは、小規模、中規模、大規模のいずれかにできます。
図 1: 一般的なアプリケーションでスコープをテストする

テストを分類する方法はたくさんあります。ただし、アプリ デベロッパーにとって最も重要な違いはテストを実行する場所です。

インストルメンテーション テストとローカルテスト

Android デバイスまたは別のパソコンでテストを実行できます。

  • インストルメンテーション テストは、物理テストまたはエミュレートされた Android デバイスで実行されます。 このアプリは、コマンドを挿入し、状態を読み取るテストアプリとともにビルドされ、インストールされます。インストルメンテーション テストは通常、アプリを起動して操作する UI テストです。
  • ローカルテストは、開発マシンまたはサーバーで実行されるため、ホスト側のテストとも呼ばれます。通常は小さくて高速で、テスト対象をアプリの他の部分から分離します。
テストは、デバイス上でのインストルメンテーション テストとして、または開発マシンでローカルテストとして実行できます。
図 2: 実行する場所に応じたテストのタイプ

すべての単体テストがローカルであるわけではなく、すべてのエンドツーエンド テストがデバイスで実行されるわけでもありません。たとえば、以下の場合です。

  • 大きなローカルテスト: Robolectric など、ローカルで実行される Android シミュレータを使用できます。
  • 小規模なインストルメンテーション テスト: SQLite データベースなどのフレームワーク機能でコードが正しく動作するかどうかを検証できます。このテストを複数のデバイスで実行して、複数のバージョンの SQLite との統合を確認できます。

次のスニペットは、インストルメンテーション UI テストで UI を操作し、要素をクリックして別の要素が表示されることを確認する方法を示しています。

Espresso

// When the Continue button is clicked
onView(withText("Continue"))
    .perform(click())

// Then the Welcome screen is displayed
onView(withText("Welcome"))
    .check(matches(isDisplayed()))

Compose UI

// When the Continue button is clicked
composeTestRule.onNodeWithText("Continue").performClick()

// Then the Welcome screen is displayed
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()

次のスニペットは、ViewModel の単体テスト(ローカル、ホスト側のテスト)の一部を示しています。

// Given an instance of MyViewModel
val viewModel = MyViewModel(myFakeDataRepository)

// When data is loaded
viewModel.loadData()

// Then it should be exposing data
assertTrue(viewModel.data != null)

テスト戦略の定義

アプリが対応しているすべてのデバイスで、アプリのすべてのコード行をテストするのが理想的です。残念ながら、この方法は時間がかかり、コストもかかるため、実用的ではありません。

優れたテスト戦略では、テストの忠実度、速度、信頼性の間で適切なバランスを見つけます。テストの忠実度は、テスト環境と実際のデバイスとの類似性によって決まります。忠実度の高いテストは、エミュレートされたデバイスまたは実機自体で実行されます。ローカル ワークステーションの JVM で低忠実度のテストが実行される場合があります。高忠実度テストは、多くの場合、処理に時間がかかり、より多くのリソースを必要とするため、すべてのテストを高忠実度テストにする必要はありません。

不安定なテスト

正しく設計および実装されたテスト実行でもエラーは発生します。たとえば、実機でテストを実行する場合、テストの途中で自動更新が開始されて、失敗することがあります。コード内の微妙な競合状態は、ごくわずかな割合で発生する可能性があります。100% の確率で合格しなかったテストは、不安定です。

テスト可能なアーキテクチャ

テスト可能なアプリ アーキテクチャでは、コードはそのさまざまな部分を個別に簡単にテストできる構造になっています。テスト可能なアーキテクチャには、読みやすさ、保守性、スケーラビリティ、再利用性など、他にもメリットがあります。

テスト不能なアーキテクチャの場合、次のようになります。

  • テストの規模が大きく、時間がかかり、不安定な場合。単体テストを行えないクラスは、より規模の大きい統合テストや UI テストでカバーしなければならない場合があります。
  • さまざまなシナリオをテストする機会が少なくなる。テストの規模が大きいと速度が落ちるため、アプリの考えられる状態をすべてテストすることは現実的ではありません。

アーキテクチャ ガイドラインについて詳しくは、アプリ アーキテクチャ ガイドをご覧ください。

分離へのアプローチ

関数、クラス、モジュールの一部を他の部分から抽出できると、テストはより簡単になり、より効果的です。この手法は分離と呼ばれ、テスト可能なアーキテクチャにとって最も重要な概念です。

一般的な分離手法には次のものがあります。

  • アプリをプレゼンテーション、ドメイン、データなどのレイヤに分割する。アプリを機能ごとに 1 つのモジュールに分割することもできます。
  • アクティビティやフラグメントなど、依存関係の大きいエンティティにはロジックを追加しないでください。これらのクラスをフレームワークへのエントリ ポイントとして使用し、UI とビジネス ロジックを別の場所(コンポーザブル、ViewModel、ドメインレイヤなど)に移動します。
  • ビジネス ロジックを含むクラスで直接のフレームワーク依存関係を避けます。たとえば、ViewModel では Android コンテキストを使用しないでください
  • 依存関係を簡単に置き換えられるようにします。たとえば、具体的な実装ではなくインターフェースを使用します。DI フレームワークを使用しない場合でも、依存関係インジェクションを使用してください。

次のステップ

テストを行う理由と、この 2 つの主なテストの種類がわかったところで、テスト項目をご覧ください。

また、初めてのテストを作成して学習する場合は、テストの Codelab をご覧ください。