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

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

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

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

分離と依存関係

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 テストとインストルメンテーション テストの両方との互換性を確保し、機能のテストや互換性の必要性に応じてテストを実行するタイミングを決定するのがおすすめです。Robolectric では Espresso テストと Compose テストの両方を実行できます。