Android でテストダブルを使用する

要素またはシステムのテスト戦略を設計する際は、関連するテストの側面が 3 つあります。

  • スコープ: テストの対象となるコードはどの程度か。テストでは、1 つのメソッド、アプリ全体、またはその中間を検証できます。テスト対象のスコープはテスト中であり、通常はテスト対象と呼ばれますが、テスト対象システムまたはテスト対象ユニットとも呼ばれます。
  • 速度: テストの実行速度。テスト速度はミリ秒から数分まで変動します。
  • 忠実度: テストはどの程度「現実的」か?たとえば、テスト対象のコードの一部がネットワーク リクエストを行う必要がある場合、テストコードは実際にこのネットワーク リクエストを行いますか?それとも、結果を偽装しますか?テストが実際にネットワークと通信する場合は、忠実度が高くなります。その反面、テストの実行時間が長くなったり、ネットワークがダウンした場合にエラーが発生する可能性や、使用コストが高くなる可能性があります。

テスト戦略の定義を開始する方法については、テスト項目をご覧ください。

分離と依存関係

要素または要素のシステムをテストする場合、テストは独立して行われます。たとえば、ViewModel をテストする場合、Android フレームワークに依存しない(または依存するべきではない)ため、エミュレータを起動して UI を起動する必要はありません。

ただし、テスト対象が他者に依存する場合があります。たとえば、ViewModel はデータ リポジトリに依存することがあります。

テスト対象に依存関係を提供する必要がある場合は、テストダブル(テスト オブジェクト)を作成するのが一般的な方法です。テストダブルは、アプリのコンポーネントとして外観と動作を行うオブジェクトですが、特定の動作やデータを提供するためにテストで作成されます。主なメリットは、テストをより迅速かつ簡単に行えることです。

テストダブルの種類

テストダブルにはさまざまな種類があります。

偽物 クラスの実装が「動作中」であるものの、テストには適しているが本番環境には適さない方法で実装されたテストダブル。

例: インメモリ データベース。

フェイクはモック フレームワークを必要とせず、軽量です。推奨されます。

モック どのように動作するようにプログラムするかを動作させ、相互作用について想定しているテストダブル。定義した要件にインタラクションが一致しない場合、モックはテストに不合格となります。モックは通常、これらすべてを実現するためのモック フレームワークを使用して作成されます。

例: データベース内のメソッドが 1 回だけ呼び出されていることを確認します。

スタブ 動作するようにプログラムされたとおりに振る舞うが、相互作用について想定していないテストダブル。通常はモック フレームワークを使用して作成されます。シンプルにするため、スタブよりもフェイクが優先されます。
ダミー 渡されるが使用されないテストダブル(パラメータとして提供するだけの場合など)。

例: クリック コールバックとして渡された空の関数。

スパイ モックと同様に、追加情報も記録する実際のオブジェクトのラッパー。通常は、複雑さを増すために回避されます。そのため、疑似やモックがスパイよりも好まれます。
シャドウ Robolectric 内で使用されるフェイク。

フェイクの使用例

UserRepository というインターフェースに依存し、最初のユーザーの名前を UI を公開する ViewModel の単体テストがあるとします。インターフェースを実装し、既知のデータを返すことで、疑似テストダブルを作成できます。

object FakeUserRepository : UserRepository {
    fun getUsers() = listOf(UserAlice, UserBob)
}

val const UserAlice = User("Alice")
val const UserBob = User("Bob")

この架空の UserRepository は、本番環境バージョンが使用するローカル データソースとリモート データソースに依存する必要がありません。このファイルはテスト ソースセットに格納され、製品版アプリには付属しません。

疑似依存関係は、リモート データソースに依存せずに既知のデータを返すことができます
図 1: 単体テストにおける疑似依存関係。

次のテストでは、ViewModel が最初のユーザー名をビューに正しく公開していることを確認します。

@Test
fun viewModelA_loadsUsers_showsFirstUser() {
    // Given a VM using fake data
    val viewModel = ViewModelA(FakeUserRepository) // Kicks off data load on init

    // Verify that the exposed data is correct
    assertEquals(viewModel.firstUserName, UserAlice.name)
}

ViewModel はテスターによって作成されるため、単体テストでは UserRepository をフェイクに置き換えるのは簡単です。ただし、大規模なテストでは任意の要素を置き換えるのが難しい場合があります。

コンポーネントの置き換えと依存関係インジェクション

テストでは、テスト対象システムの作成を制御できない場合、テストダブル用のコンポーネントの置き換えはより複雑になり、アプリのアーキテクチャはテスト可能な設計に従う必要があります。

アプリ内のユーザーフロー全体をナビゲートするインストルメンテーション UI テストなど、大規模なエンドツーエンド テストでテストダブルを使用するとメリットが得られます。その場合は、テストを密閉型にすることをおすすめします。密閉型テストでは、インターネットからのデータの取得など、外部依存関係がすべて回避されます。これにより、信頼性とパフォーマンスが向上します。

図 2: アプリのほとんどをカバーし、疑似リモートデータをカバーする大規模なテスト。

この柔軟性を実現するようにアプリを設計することもできますが、Hilt などの依存関係インジェクション フレームワークを使用して、テスト時にアプリ内のコンポーネントを置き換えることをおすすめします。Hilt テストガイドをご覧ください。

Robolectric

Android では、特殊なタイプのテストダブルを提供する Robolectric フレームワークを使用できます。Robolectric を使用すると、ワークステーションまたは継続的インテグレーション環境でテストを実行できます。エミュレータやデバイスなしで、通常の JVM を使用します。シャドウと呼ばれるテストダブルを使用して、ビューのインフレーション、リソースの読み込みなど、Android フレームワークの他の部分をシミュレートします。

Robolectric はシミュレータであるため、単純な単体テストの代わりになるものや、互換性テストに使用するべきではありません。速度が向上し、コストが削減されますが、場合によっては忠実度が低下します。UI テストには、Robolectric テストとインストルメンテーション テストの両方と互換性を持たせ、機能または互換性をテストする必要性に応じて実行するタイミングを決定する方法をおすすめします。Espresso と Compose のどちらのテストも Robolectric で実行できます。