テストダブルと依存関係注入の概要

この Codelab は、Kotlin を使った高度な Android 開発コースの一部です。Codelab を順番に進めると、このコースを最大限に活用できますが、これは必須ではありません。コースの Codelab はすべて、Kotlin を使った高度な Android 開発の Codelab ランディング ページに記載されています。

はじめに

この 2 つ目のテスト Codelab では、テストダブルについて説明します。Android でテストダブルを使用するタイミングや、依存性注入、サービス ロケータ パターン、ライブラリを使用してテストダブルを実装する方法について学びます。この過程で、次の方法を学習します。

  • リポジトリの単体テスト
  • フラグメントと ViewModel の統合テスト
  • フラグメント ナビゲーションのテスト

前提となる知識

以下について把握しておく必要があります。

学習内容

  • テスト戦略を計画する方法
  • テストダブル(フェイクとモック)の作成方法と使用方法
  • Android で単体テストと統合テストに手動の依存関係インジェクションを使用する方法
  • サービス ロケータ パターンの適用方法
  • リポジトリ、フラグメント、ビューモデル、Navigation コンポーネントをテストする方法

次のライブラリとコードのコンセプトを使用します。

演習内容

  • テスト ダブルと依存関係の注入を使用して、リポジトリの単体テストを作成する。
  • テストダブルと依存関係の注入を使用して、ビューモデルの単体テストを作成します。
  • Espresso UI テスト フレームワークを使用して、フラグメントとそのビューモデルの統合テストを作成します。
  • Mockito と Espresso を使用してナビゲーション テストを作成します。

この一連の Codelab では、TO-DO Notes アプリを使用します。このアプリでは、完了するタスクを書き留めてリストに表示できます。完了または未完了としてマークしたり、フィルタしたり、削除したりできます。

このアプリは Kotlin で記述されており、いくつかの画面があり、Jetpack コンポーネントを使用し、アプリ アーキテクチャ ガイドのアーキテクチャに沿っています。このアプリのテスト方法を学ぶことで、同じライブラリとアーキテクチャを使用するアプリをテストできるようになります。

コードをダウンロードする

まず、コードをダウンロードします。

ZIP をダウンロード

または、コードの GitHub リポジトリのクローンを作成することもできます。

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_1

以下の手順に沿って、コードをよく理解してください。

ステップ 1: サンプルアプリを実行する

TO-DO アプリをダウンロードしたら、Android Studio で開いて実行します。コンパイルされるはずです。次の手順に沿って、アプリを確認します。

  • フローティング アクション ボタンのプラスアイコンを使用して、新しいタスクを作成します。まずタイトルを入力し、次にタスクに関する追加情報を入力します。緑色のチェックマークの FAB で保存します。
  • タスクのリストで、完了したタスクのタイトルをクリックし、そのタスクの詳細画面で残りの説明を確認します。
  • リストまたは詳細画面で、タスクのチェックボックスをオンにして、ステータスを [完了] に設定します。
  • タスク画面に戻り、フィルタ メニューを開いて、[Active](有効)と [Completed](完了)のステータスでタスクをフィルタします。
  • ナビゲーション ドロワーを開き、[統計情報] をクリックします。
  • 概要画面に戻り、ナビゲーション ドロワー メニューから [完了済みをクリア] を選択して、ステータスが [完了] のタスクをすべて削除します。

ステップ 2: サンプルアプリのコードを確認する

TO-DO アプリは、一般的な Architecture Blueprints テストとアーキテクチャのサンプル(サンプルのリアクティブ アーキテクチャ バージョンを使用)に基づいています。アプリは、アプリ アーキテクチャ ガイドのアーキテクチャに沿って実行されます。フラグメント、リポジトリ、Room で ViewModel を使用します。以下の例のいずれかをご存知であれば、このアプリのアーキテクチャも同様です。

1 つのレイヤのロジックを深く理解するよりも、アプリの一般的なアーキテクチャを理解する方が重要です。

パッケージの概要は次のとおりです。

パッケージ: com.example.android.architecture.blueprints.todoapp

.addedittask

タスクの追加または編集画面: タスクの追加または編集用の UI レイヤコード。

.data

データレイヤー: タスクのデータレイヤーを処理します。データベース、ネットワーク、リポジトリのコードが含まれています。

.statistics

統計情報画面: 統計情報画面の UI レイヤコード。

.taskdetail

タスクの詳細画面: 単一のタスクの UI レイヤコード。

.tasks

タスク画面: すべてのタスクのリストの UI レイヤコード。

.util

ユーティリティ クラス: アプリのさまざまな部分で使用される共有クラス(複数の画面で使用されるスワイプ更新レイアウトなど)。

データレイヤ(.data)

このアプリには、remote パッケージのシミュレートされたネットワーキング レイヤと、local パッケージのデータベース レイヤが含まれています。このプロジェクトでは、簡略化のため、実際のネットワーク リクエストを行うのではなく、遅延のある HashMap のみでネットワーク レイヤをシミュレートします。

DefaultTasksRepository は、ネットワーク レイヤとデータベース レイヤの間を調整または仲介し、UI レイヤにデータを返します。

UI レイヤ(.addedittask、.statistics、.taskdetail、.tasks)

UI レイヤの各パッケージには、フラグメントとビューモデル、および UI に必要なその他のクラス(タスクリストのアダプタなど)が含まれています。TaskActivity は、すべてのフラグメントを含むアクティビティです。

ナビゲーション

アプリのナビゲーションは Navigation コンポーネントによって制御されます。これは nav_graph.xml ファイルで定義されます。ナビゲーションは、Event クラスを使用してビューモデルでトリガーされます。ビューモデルは、渡す引数も決定します。フラグメントは Event を監視し、画面間の実際のナビゲーションを行います。

この Codelab では、テスト ダブルと依存関係インジェクションを使用して、リポジトリ、ビューモデル、フラグメントをテストする方法を学びます。これらのテストについて説明する前に、テストの対象と方法を決定する際の根拠を理解しておくことが重要です。

このセクションでは、Android に適用される一般的なテストのベスト プラクティスについて説明します。

テスト ピラミッド

テスト戦略を考える際には、次の 3 つの関連するテストの側面があります。

  • 範囲 - テストがコードのどの程度に影響するか。テストは、単一のメソッド、アプリケーション全体、またはその間のどこかで実行できます。
  • 速度 - テストの実行速度はどのくらいですか?テスト速度は、ミリ秒から数分までさまざまです。
  • 忠実度 - テストはどの程度「現実世界」に近いか。たとえば、テスト対象のコードの一部でネットワーク リクエストを行う必要がある場合、テストコードは実際にこのネットワーク リクエストを行うのか、それとも結果を偽装するのか。テストが実際にネットワークと通信する場合、忠実度が高くなります。ただし、テストの実行に時間がかかる、ネットワークがダウンしている場合はエラーが発生する、使用コストが高いといったデメリットがあります。

これらの側面の間には、本質的なトレードオフがあります。たとえば、速度と忠実度はトレードオフの関係にあります。一般的に、テストが速いほど忠実度は低くなり、その逆も同様です。自動テストを分類する一般的な方法の 1 つは、次の 3 つのカテゴリに分類することです。

  • 単体テスト - 1 つのクラス(通常はそのクラス内の 1 つのメソッド)で実行される、非常に焦点を絞ったテストです。単体テストが失敗した場合、コードのどこに問題があるかを正確に把握できます。ただし、実際のアプリでは 1 つのメソッドやクラスの実行よりもはるかに多くの処理が行われるため、忠実度は低くなります。コードを変更するたびに実行できるほど高速です。通常は、ローカルで実行されるテスト(test ソースセット内)になります。例: ビューモデルとリポジトリの単一メソッドをテストします。
  • 統合テスト - 複数のクラスの相互作用をテストし、それらを一緒に使用したときに期待どおりに動作することを確認します。統合テストを構成する方法の 1 つは、タスクを保存する機能など、単一の機能をテストすることです。単体テストよりも広い範囲のコードをテストしますが、完全な忠実度ではなく、高速で実行できるように最適化されています。状況に応じて、ローカルで実行することも、インストルメンテーション テストとして実行することもできます。例: 単一のフラグメントとビューモデルのペアのすべての機能をテストします。
  • エンドツーエンド テスト(E2e) - 連携して動作する機能の組み合わせをテストします。アプリの大部分をテストし、実際の使用状況を忠実にシミュレートするため、通常は実行に時間がかかります。忠実度が最も高く、アプリケーション全体が実際に機能していることを確認できます。一般的に、これらのテストはインストルメント化テスト(androidTest ソースセット内)になります。
    例: アプリ全体を起動して、いくつかの機能をまとめてテストします。

これらのテストの推奨される割合は、ピラミッドで表されることが多く、テストの大部分は単体テストです。

アーキテクチャとテスト

テスト ピラミッドのさまざまなレベルでアプリをテストできるかどうかは、アプリのアーキテクチャに本質的に関連しています。たとえば、アーキテクチャが非常に貧弱なアプリケーションでは、すべてのロジックが 1 つのメソッド内に配置されている可能性があります。これらのテストはアプリの大部分をテストする傾向があるため、エンドツーエンド テストを作成できるかもしれませんが、単体テストや統合テストを作成する場合はどうでしょうか?すべてのコードが 1 か所に集約されているため、単一のユニットまたは機能に関連するコードのみをテストすることは困難です。

より適切な方法は、アプリケーション ロジックを複数のメソッドとクラスに分割し、各部分を個別にテストできるようにすることです。アーキテクチャは、コードを分割して整理する方法であり、単体テストと統合テストを容易にします。テストする ToDo アプリは、特定のアーキテクチャに従っています。



このレッスンでは、上記のアーキテクチャの一部を適切に分離してテストする方法について説明します。

  1. まず、リポジトリの単体テストを行います。
  2. 次に、ビューモデルでテスト ダブルを使用します。これは、ビューモデルの単体テストと統合テストに必要です。
  3. 次に、フラグメントとそのビューモデルの統合テストを作成する方法を学びます。
  4. 最後に、Navigation コンポーネントを含む統合テストを作成する方法を学びます。

エンドツーエンド テストについては、次のレッスンで説明します。

クラスの一部(メソッドまたはメソッドの小さなコレクション)の単体テストを作成する場合、目標はそのクラスのコードのみをテストすることです。

特定のクラスのコードのみをテストするのは難しい場合があります。次のような例を考えてみましょう。main ソースセットで data.source.DefaultTaskRepository クラスを開きます。これはアプリのリポジトリであり、次に単体テストを作成するクラスです。

目標は、そのクラスのコードのみをテストすることです。ただし、DefaultTaskRepository は、LocalTaskDataSource や RemoteTaskDataSource などの他のクラスに依存して機能します。別の言い方をすると、LocalTaskDataSource と RemoteTaskDataSource は DefaultTaskRepository の依存関係です。

そのため、DefaultTaskRepository のすべてのメソッドはデータソース クラスのメソッドを呼び出し、そのメソッドは他のクラスのメソッドを呼び出して、情報をデータベースに保存したり、ネットワークと通信したりします。



たとえば、DefaultTasksRepo の次のメソッドを見てみましょう。

    suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> {
        if (forceUpdate) {
            try {
                updateTasksFromRemoteDataSource()
            } catch (ex: Exception) {
                return Result.Error(ex)
            }
        }
        return tasksLocalDataSource.getTasks()
    }

getTasks は、リポジトリに対して行う最も基本的な呼び出しの 1 つです。このメソッドには、SQLite データベースからの読み取りとネットワーク呼び出し(updateTasksFromRemoteDataSource の呼び出し)が含まれます。これには、リポジトリ コードだけよりもはるかに多くのコードが含まれます。

リポジトリのテストが難しい理由を具体的に説明します。

  • このリポジトリの最も簡単なテストを行う場合でも、データベースの作成と管理について考える必要があります。これにより、「これはローカル テストかインストルメンテーション テストか?」や「AndroidX Test を使用して Android 環境をシミュレートすべきか?」などの疑問が生じます。
  • ネットワーキング コードなど、コードの一部は実行に時間がかかったり、失敗したりすることがあり、実行時間が長く、不安定なテストが作成されることがあります。
  • テストの失敗の原因となったコードを診断するテストの機能が失われる可能性があります。テストでリポジトリ以外のコードのテストが開始される可能性があります。たとえば、依存コード(データベース コードなど)の問題が原因で、「リポジトリ」の単体テストが失敗する可能性があります。

テストダブル

この解決策は、リポジトリをテストする際に、実際のネットワーク コードやデータベース コードを使用せず、テスト ダブルを使用することです。テストダブルは、テスト用に特別に作成されたクラスのバージョンです。テストでクラスの実際のバージョンを置き換えることを目的としています。スタント ダブルがスタントを専門とする俳優で、危険なアクションの際に実際の俳優に代わるのと同様です。

テスト ダブルには次のような種類があります。

偽物

クラスの「動作する」実装を持つテストダブル。テストには適しているが、本番環境には適していない方法で実装されています。

モック

どのメソッドが呼び出されたかを追跡するテストダブル。メソッドが正しく呼び出されたかどうかに応じて、テストに合格または不合格となります。

スタブ

ロジックを含まず、プログラムされた値を返すだけのテストダブル。たとえば、StubTaskRepository は getTasks から特定のタスクの組み合わせを返すようにプログラムできます。

ダミー

パラメータとして提供する必要がある場合など、渡されるだけで使用されないテストダブル。NoOpTaskRepository がある場合、メソッドにコードを記述せずに TaskRepository を実装するだけです。

Spy

追加情報も追跡するテストダブル。たとえば、SpyTaskRepository を作成した場合、addTask メソッドが呼び出された回数を追跡できます。

テスト ダブルの詳細については、Testing on the Toilet: Know Your Test Doubles をご覧ください。

Android で最もよく使用されるテストダブルは、フェイクとモックです。

このタスクでは、実際のデータソースから切り離された DefaultTasksRepository を単体テストするために、FakeDataSource テストダブルを作成します。

ステップ 1: FakeDataSource クラスを作成する

このステップでは、LocalDataSource と RemoteDataSource のテストダブルとなる FakeDataSouce というクラスを作成します。

  1. test ソースセットで右クリックし、[New] -> [Package] を選択します。

  1. ソース パッケージを含むデータ パッケージを作成します。
  2. data/source パッケージに FakeDataSource という新しいクラスを作成します。

ステップ 2: TasksDataSource インターフェースを実装する

新しいクラス FakeDataSource をテストダブルとして使用できるようにするには、他のデータソースを置き換えることができる必要があります。これらのデータソースは TasksLocalDataSource と TasksRemoteDataSource です。

  1. どちらも TasksDataSource インターフェースを実装していることに注意してください。
class TasksLocalDataSource internal constructor(
    private val tasksDao: TasksDao,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }

object TasksRemoteDataSource : TasksDataSource { ... }
  1. FakeDataSource に TasksDataSource を実装します。
class FakeDataSource : TasksDataSource {

}

Android Studio は、TasksDataSource の必須メソッドが実装されていないことを示すエラーを表示します。

  1. クイック フィックス メニューを使用して、[メンバーを実装] を選択します。


  1. すべてのメソッドを選択して、[OK] を押します。

ステップ 3: FakeDataSource で getTasks メソッドを実装する

FakeDataSource は、フェイクと呼ばれる特定の種類のテスト ダブルです。フェイクは、クラスの「動作する」実装を持つテスト ダブルですが、テストには適しているものの本番環境には適していない方法で実装されています。「動作する」実装とは、入力に対して現実的な出力を生成するクラスを意味します。

たとえば、偽のデータソースはネットワークに接続したり、データベースに何かを保存したりしません。代わりに、メモリ内のリストを使用します。タスクを取得または保存するメソッドが期待どおりの結果を返すため、これは「期待どおりに動作」しますが、サーバーやデータベースに保存されないため、この実装を本番環境で使用することはできません。

A: FakeDataSource

  • を使用すると、実際のデータベースやネットワークに依存することなく、DefaultTasksRepository のコードをテストできます。
  • テスト用の「十分な」実装を提供します。
  1. FakeDataSource コンストラクタを変更して、tasks という var を作成します。これは、デフォルト値が空の可変リストである MutableList<Task>? です。
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }


データベースまたはサーバーのレスポンスを「偽装」するタスクのリストです。ここでは、リポジトリの getTasks メソッドをテストすることを目標とします。これにより、データソースの getTasks、deleteAllTasks、saveTask メソッドが呼び出されます。

これらのメソッドのフェイク バージョンを作成します。

  1. 書き込み getTasks: tasks が null でない場合は、Success の結果を返します。tasks が null の場合、Error の結果を返します。
  2. 書き込み deleteAllTasks: ミュータブルなタスクリストをクリアします。
  3. saveTask: リストにタスクを追加します。

FakeDataSource 用に実装されたこれらのメソッドは、次のコードのようになります。

override suspend fun getTasks(): Result<List<Task>> {
    tasks?.let { return Success(ArrayList(it)) }
    return Error(
        Exception("Tasks not found")
    )
}


override suspend fun deleteAllTasks() {
    tasks?.clear()
}

override suspend fun saveTask(task: Task) {
    tasks?.add(task)
}

必要に応じて、次の import ステートメントを使用します。

import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task

これは、実際のローカル データソースとリモート データソースの仕組みに似ています。

このステップでは、手動依存関係注入という手法を使用して、作成したばかりのテスト ダブルのフェイクを使用できるようにします。

主な問題は、FakeDataSource があるものの、テストでどのように使用するかが不明なことです。TasksRemoteDataSource と TasksLocalDataSource を置き換える必要がありますが、テスト内でのみです。TasksRemoteDataSource と TasksLocalDataSource はどちらも DefaultTasksRepository の依存関係です。つまり、DefaultTasksRepositories はこれらのクラスを実行するために必要です。

現在、依存関係は DefaultTasksRepository の init メソッド内で構築されています。

DefaultTasksRepository.kt

class DefaultTasksRepository private constructor(application: Application) {

    private val tasksRemoteDataSource: TasksDataSource
    private val tasksLocalDataSource: TasksDataSource

   // Some other code

    init {
        val database = Room.databaseBuilder(application.applicationContext,
            ToDoDatabase::class.java, "Tasks.db")
            .build()

        tasksRemoteDataSource = TasksRemoteDataSource
        tasksLocalDataSource = TasksLocalDataSource(database.taskDao())
    }
    // Rest of class
}

DefaultTasksRepository の内部で taskLocalDataSource と tasksRemoteDataSource を作成して割り当てているため、これらは基本的にハードコードされています。テストダブルを入れ替える方法はありません。

代わりに、これらのデータソースをハードコードするのではなく、クラスに提供します。依存関係の提供は、依存関係インジェクションと呼ばれます。依存関係の提供方法にはさまざまなものがあり、依存性注入にもさまざまな種類があります。

コンストラクタ依存関係注入を使用すると、テストダブルをコンストラクタに渡すことで、テストダブルを入れ替えることができます。

インジェクションなし

インジェクション

ステップ 1: DefaultTasksRepository でコンストラクタ依存性注入を使用する

  1. DefaultTaskRepository のコンストラクタを、Application を受け取るものから、データソースとコルーチン ディスパッチャーの両方を受け取るものに変更します(テスト用にスワップする必要もあります。詳しくは、コルーチンの 3 つ目のレッスン セクションをご覧ください)。

DefaultTasksRepository.kt

// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }

// WITH

class DefaultTasksRepository(
    private val tasksRemoteDataSource: TasksDataSource,
    private val tasksLocalDataSource: TasksDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }
  1. 依存関係を渡したので、init メソッドを削除します。依存関係を作成する必要はなくなりました。
  2. 古いインスタンス変数も削除します。コンストラクタで定義します。

DefaultTasksRepository.kt

// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
  1. 最後に、新しいコンストラクタを使用するように getRepository メソッドを更新します。

DefaultTasksRepository.kt

    companion object {
        @Volatile
        private var INSTANCE: DefaultTasksRepository? = null

        fun getRepository(app: Application): DefaultTasksRepository {
            return INSTANCE ?: synchronized(this) {
                val database = Room.databaseBuilder(app,
                    ToDoDatabase::class.java, "Tasks.db")
                    .build()
                DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                    INSTANCE = it
                }
            }
        }
    }

これでコンストラクタ依存関係の挿入を使用できるようになりました。

ステップ 2: テストで FakeDataSource を使用する

コードでコンストラクタ依存性注入が使用されるようになったので、フェイク データソースを使用して DefaultTasksRepository をテストできます。

  1. DefaultTasksRepository クラス名を右クリックして、[Generate]、[Test] の順に選択します。
  2. 画面の指示に沿って、test ソースセットに DefaultTasksRepositoryTest を作成します。
  3. 新しい DefaultTasksRepositoryTest クラスの先頭に、以下のメンバー変数を追加して、フェイク データソースのデータを表します。

DefaultTasksRepositoryTest.kt

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }
  1. 3 つの変数(2 つの FakeDataSource メンバー変数(リポジトリのデータソースごとに 1 つ)と、テストする DefaultTasksRepository の変数)を作成します。

DefaultTasksRepositoryTest.kt

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

テスト可能な DefaultTasksRepository を設定して初期化するメソッドを作成します。この DefaultTasksRepository は、テストダブル FakeDataSource を使用します。

  1. createRepository というメソッドを作成し、@Before アノテーションを付けます。
  2. remoteTasks リストと localTasks リストを使用して、フェイク データソースをインスタンス化します。
  3. 作成した 2 つのフェイク データソースと Dispatchers.Unconfined を使用して、tasksRepository をインスタンス化します。

最終的なメソッドは次のようになります。

DefaultTasksRepositoryTest.kt

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

ステップ 3: DefaultTasksRepository の getTasks() テストを作成する

DefaultTasksRepository テストを作成しましょう。

  1. リポジトリの getTasks メソッドのテストを作成します。true で getTasks を呼び出すと(リモート データソースから再読み込みされるはず)、(ローカル データソースではなく)リモート データソースからデータが返されることを確認します。

DefaultTasksRepositoryTest.kt

@Test
    fun getTasks_requestsAllTasksFromRemoteDataSource(){
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

getTasks: を呼び出すとエラーが発生する

ステップ 4: runBlockingTest を追加する

getTasks は suspend 関数であり、呼び出すにはコルーチンを起動する必要があるため、コルーチン エラーは想定どおりです。そのためには、コルーチン スコープが必要です。このエラーを解決するには、テストでコルーチンの起動を処理するための Gradle 依存関係を追加する必要があります。

  1. testImplementation を使用して、コルーチンをテストするために必要な依存関係をテスト ソースセットに追加します。

app/build.gradle

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

同期をお忘れなく。

kotlinx-coroutines-test は、コルーチンをテストするために特別に設計されたコルーチン テスト ライブラリです。テストを実行するには、関数 runBlockingTest を使用します。これは、コルーチン テスト ライブラリが提供する関数です。コードブロックを受け取り、同期的に即座に実行される特別なコルーチン コンテキストでこのコードブロックを実行します。つまり、アクションは決定論的な順序で発生します。これにより、コルーチンはコルーチンではないかのように実行されます。これはコードのテストを目的としています。

suspend 関数を呼び出す場合は、テストクラスで runBlockingTest を使用します。runBlockingTest の仕組みとコルーチンのテスト方法については、このシリーズの次の Codelab で詳しく説明します。

  1. クラスの上に @ExperimentalCoroutinesApi を追加します。これは、クラスで試験運用版のコルーチン API(runBlockingTest)を使用していることを示すものです。これがないと、警告が表示されます。
  2. DefaultTasksRepositoryTest に戻り、テスト全体をコードの「ブロック」として受け取るように runBlockingTest を追加します。

最終的なテストは次のようになります。

DefaultTasksRepositoryTest.kt

import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test


@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

    @Test
    fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

}
  1. 新しい getTasks_requestsAllTasksFromRemoteDataSource テストを実行し、機能することとエラーが解消されたことを確認します。

リポジトリの単体テストを行う方法について説明しました。次のステップでは、依存性注入を再び使用して別のテスト ダブルを作成します。今回は、ビューモデルの単体テストと統合テストの作成方法を示します。

単体テストでは、対象のクラスまたはメソッドのみをテストする必要があります。これは、分離テストと呼ばれます。このテストでは、「ユニット」を明確に分離し、そのユニットの一部であるコードのみをテストします。

したがって、TasksViewModelTest は TasksViewModel コードのみをテストする必要があります。データベース、ネットワーク、リポジトリ クラスのテストは行わないでください。そのため、ビューモデルでは、リポジトリで行ったのと同様に、テストで使用するフェイク リポジトリを作成し、依存関係インジェクションを適用します。

このタスクでは、依存性注入をビューモデルに適用します。

ステップ 1. TasksRepository インターフェースを作成する

コンストラクタ依存性注入を使用する最初のステップは、偽のクラスと実際のクラスで共有される共通のインターフェースを作成することです。

実際にはどのような仕組みになっているのでしょうか?TasksRemoteDataSource、TasksLocalDataSource、FakeDataSource を見てください。これらはすべて同じインターフェース TasksDataSource を共有しています。これにより、DefaultTasksRepository のコンストラクタで TasksDataSource を受け取ることを指定できます。

DefaultTasksRepository.kt

class DefaultTasksRepository(
   private val tasksRemoteDataSource: TasksDataSource,
   private val tasksLocalDataSource: TasksDataSource,
   private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {

これにより、FakeDataSource をスワップできます。

次に、データソースの場合と同様に、DefaultTasksRepository のインターフェースを作成します。DefaultTasksRepository のすべての公開メソッド(公開 API サーフェス)を含める必要があります。

  1. DefaultTasksRepository を開き、クラス名を右クリックします。[Refactor -> Extract -> Interface] を選択します。

  1. [別のファイルに抽出] を選択します。

  1. [Extract Interface] ウィンドウで、インターフェース名を TasksRepository に変更します。
  2. [Members to form interface] セクションで、2 つのコンパニオン メンバーと private メソッドを除くすべてのメンバーをチェックします。


  1. [リファクタリング] をクリックします。新しい TasksRepository インターフェースが data/source パッケージに表示されます。

また、DefaultTasksRepository に TasksRepository が実装されました。

  1. アプリ(テストではない)を実行して、すべてが正常に動作することを確認します。

ステップ 2. FakeTestRepository を作成する

インターフェースができたので、DefaultTaskRepository テストダブルを作成できます。

  1. テスト ソースセットの data/source で、Kotlin ファイルとクラス FakeTestRepository.kt を作成し、TasksRepository インターフェースから拡張します。

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

インターフェース メソッドを実装する必要があることが通知されます。

  1. エラーにカーソルを合わせ、候補メニューが表示されたら、クリックして [Implement members] を選択します。
  1. すべてのメソッドを選択して、[OK] を押します。

ステップ 3. FakeTestRepository メソッドを実装する

これで、「未実装」のメソッドを含む FakeTestRepository クラスが作成されました。FakeDataSource を実装したときと同様に、FakeTestRepository はローカル データソースとリモート データソース間の複雑なメディエーションを処理するのではなく、データ構造によってサポートされます。

FakeTestRepository は FakeDataSource などを利用する必要はありません。入力に対して現実的な偽の出力を返すだけで十分です。LinkedHashMap を使用してタスクのリストを保存し、MutableLiveData を使用してオブザーバブル タスクを保存します。

  1. FakeTestRepository で、現在のタスクのリストを表す LinkedHashMap 変数と、オブザーバブル タスクの MutableLiveData の両方を追加します。

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()


    // Rest of class
}

次のメソッドを実装します。

  1. getTasks - このメソッドは、tasksServiceData を受け取り、tasksServiceData.values.toList() を使用してリストに変換し、Success の結果として返します。
  2. refreshTasks - observableTasks の値を getTasks() から返された値に更新します。
  3. observeTasks - runBlocking を使用してコルーチンを作成し、refreshTasks を実行して observableTasks を返します。

以下は、これらのメソッドのコードです。

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        return Result.Success(tasksServiceData.values.toList())
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    // Rest of class

}

ステップ 4. テスト用のメソッドを addTasks に追加

テストの際は、リポジトリに Tasks がいくつかある方が望ましいです。saveTask を複数回呼び出すこともできますが、これを簡単にするために、タスクを追加できるテスト専用のヘルパー メソッドを追加します。

  1. addTasks メソッドを追加します。このメソッドはタスクの vararg を受け取り、それぞれを HashMap に追加してから、タスクを更新します。

FakeTestRepository.kt

    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }

この時点で、テスト用の偽のリポジトリが作成され、いくつかの主要なメソッドが実装されています。次に、これをテストで使用します。

このタスクでは、ViewModel 内でフェイク クラスを使用します。コンストラクタ依存性注入を使用して、TasksViewModel のコンストラクタに TasksRepository 変数を追加することで、コンストラクタ依存性注入を介して 2 つのデータソースを取り込みます。

ビューモデルは直接構築しないため、このプロセスは少し異なります。例:

class TasksFragment : Fragment() {

    private val viewModel by viewModels<TasksViewModel>()
    
    // Rest of class...

}


上記のコードのように、ビューモデルを作成する viewModel's プロパティ委任を使用しています。ビューモデルの構築方法を変更するには、ViewModelProvider.Factory を追加して使用する必要があります。ViewModelProvider.Factory について詳しくは、こちらをご覧ください。

ステップ 1. TasksViewModel で ViewModelFactory を作成して使用する

まず、Tasks 画面に関連するクラスとテストを更新します。

  1. 開く TasksViewModel。
  2. TasksViewModel のコンストラクタを変更して、クラス内で構築するのではなく TasksRepository を受け取るようにします。

TasksViewModel.kt

// REPLACE
class TasksViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() { 
    // Rest of class 
}

コンストラクタを変更したので、ファクトリーを使用して TasksViewModel を構築する必要があります。ファクトリ クラスは TasksViewModel と同じファイルに配置しますが、独自のファイルに配置することもできます。

  1. TasksViewModel ファイルのクラス外の下部に、プレーンな TasksRepository を受け取る TasksViewModelFactory を追加します。

TasksViewModel.kt

@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TasksViewModel(tasksRepository) as T)
}


これは、ViewModel の構築方法を変更する標準的な方法です。ファクトリが用意できたら、ビューモデルを構築する場所でファクトリを使用します。

  1. ファクトリーを使用するように TasksFragment を更新します。

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TasksViewModel>()

// WITH

private val viewModel by viewModels<TasksViewModel> {
    TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. アプリのコードを実行して、すべてが正常に機能していることを確認します。

ステップ 2. TasksViewModelTest 内で FakeTestRepository を使用する

これで、ビューモデルのテストで実際のリポジトリの代わりに、フェイク リポジトリを使用できるようになりました。

  1. TasksViewModelTest を開きます。
  2. TasksViewModelTest に FakeTestRepository プロパティを追加します。

TaskViewModelTest.kt

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Use a fake repository to be injected into the viewmodel
    private lateinit var tasksRepository: FakeTestRepository
    
    // Rest of class
}
  1. setupViewModel メソッドを更新して 3 つのタスクを含む FakeTestRepository を作成し、このリポジトリを使用して tasksViewModel を構築します。

TasksViewModelTest.kt

    @Before
    fun setupViewModel() {
        // We initialise the tasks to 3, with one active and two completed
        tasksRepository = FakeTestRepository()
        val task1 = Task("Title1", "Description1")
        val task2 = Task("Title2", "Description2", true)
        val task3 = Task("Title3", "Description3", true)
        tasksRepository.addTasks(task1, task2, task3)

        tasksViewModel = TasksViewModel(tasksRepository)
        
    }
  1. AndroidX Test ApplicationProvider.getApplicationContext コードを使用しなくなったため、@RunWith(AndroidJUnit4::class) アノテーションも削除できます。
  2. テストを実行して、すべてが正常に機能することを確認します。

コンストラクタの依存関係の挿入を使用することで、DefaultTasksRepository を依存関係として削除し、テストで FakeTestRepository に置き換えました。

ステップ 3. TaskDetail フラグメントと ViewModel も更新する

TaskDetailFragment と TaskDetailViewModel にもまったく同じ変更を加えます。これにより、次に TaskDetail テストを作成する際にコードを準備できます。

  1. 開く TaskDetailViewModel。
  2. コンストラクタを更新します。

TaskDetailViewModel.kt

// REPLACE
class TaskDetailViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TaskDetailViewModel(
    private val tasksRepository: TasksRepository
) : ViewModel() { // Rest of class }
  1. TaskDetailViewModel ファイルのクラス外の下部に TaskDetailViewModelFactory を追加します。

TaskDetailViewModel.kt

@Suppress("UNCHECKED_CAST")
class TaskDetailViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TaskDetailViewModel(tasksRepository) as T)
}
  1. ファクトリーを使用するように TasksFragment を更新します。

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()

// WITH

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. コードを実行して、すべてが正常に機能することを確認します。

TasksFragment と TasksDetailFragment で、実際のリポジトリの代わりに FakeTestRepository を使用できるようになりました。

次に、フラグメントとビューモデルのインタラクションをテストする統合テストを作成します。ビューモデルのコードが UI を適切に更新するかどうかを確認できます。これを行うには、

  • ServiceLocator パターン
  • Espresso ライブラリと Mockito ライブラリ

統合テスト では、複数のクラスの相互作用をテストして、それらを一緒に使用したときに想定どおりに動作することを確認します。これらのテストは、ローカル(test ソースセット)またはインストルメンテーション テスト(androidTest ソースセット)として実行できます。

この場合、各フラグメントを取得し、フラグメントとビューモデルの統合テストを記述して、フラグメントの主な機能をテストします。

ステップ 1. Gradle の依存関係を追加する

  1. 次の Gradle 依存関係を追加します。

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "junit:junit:$junitVersion"
    androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

    // Testing code should not be included in the main code.
    // Once https://issuetracker.google.com/128612536 is fixed this can be fixed.

    implementation "androidx.fragment:fragment-testing:$fragmentVersion"
    implementation "androidx.test:core:$androidXTestCoreVersion"

これらの依存関係には、次のものがあります。

  • junit:junit - 基本的なテスト ステートメントの作成に必要な JUnit。
  • androidx.test:core - コア AndroidX テスト ライブラリ
  • kotlinx-coroutines-test - コルーチン テスト ライブラリ
  • androidx.fragment:fragment-testing - テストでフラグメントを作成し、その状態を変更するための AndroidX テスト ライブラリ。

これらのライブラリは androidTest ソースセットで使用するため、androidTestImplementation を使用して依存関係として追加します。

ステップ 2. TaskDetailFragmentTest クラスを作成する

TaskDetailFragment は、単一のタスクに関する情報を表示します。

他のフラグメントと比較して基本的な機能しか持たない TaskDetailFragment のフラグメント テストから作成します。

  1. 開く taskdetail.TaskDetailFragment。
  2. これまでと同様に、TaskDetailFragment のテストを生成します。デフォルトの選択をそのまま使用して、androidTest ソースセット(test ソースセットではない)に配置します。

  1. TaskDetailFragmentTest クラスに次のアノテーションを追加します。

TaskDetailFragmentTest.kt

@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

}

これらのアノテーションの目的は次のとおりです。

  • @MediumTest - テストを「中程度の実行時間」の統合テストとしてマークします(@SmallTest 単体テストや @LargeTest 大規模なエンドツーエンド テストとは異なります)。これにより、テストをグループ化して、実行するテストのサイズを選択できます。
  • @RunWith(AndroidJUnit4::class) - AndroidX Test を使用するクラスで使用されます。

ステップ 3. テストからフラグメントを起動する

このタスクでは、AndroidX Testing ライブラリを使用して TaskDetailFragment を起動します。FragmentScenario は、フラグメントをラップし、テスト用にフラグメントのライフサイクルを直接制御できるようにする AndroidX Test のクラスです。フラグメントのテストを作成するには、テストするフラグメント(TaskDetailFragment)の FragmentScenario を作成します。

  1. このテストを TaskDetailFragmentTest にコピーします。

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

上記のコードは次のようになります。

  • タスクを作成します。
  • Bundle を作成します(フラグメントに渡されるタスクのフラグメント引数を表します)。
  • launchFragmentInContainer 関数は、このバンドルとテーマを使用して FragmentScenario を作成します。

このテストはまだ完了していません。何もアサートしていないためです。ここでは、テストを実行して何が起こるかを確認します。

  1. これはインストルメンテーション テストなので、エミュレータまたはデバイスが表示されていることを確認してください。
  2. テストを実行します。

次のようになります。

  • まず、これはインストルメンテーション テストであるため、テストは物理デバイス(接続されている場合)またはエミュレータで実行されます。
  • フラグメントが起動します。
  • 他のフラグメントをナビゲートしたり、アクティビティに関連付けられたメニューを表示したりしないことに注目してください。これは、単なるフラグメントです。

最後に、フラグメントがタスクデータを正常に読み込んでいないため、「データなし」と表示されていることを確認します。

テストでは、TaskDetailFragment を読み込む(これは完了済み)とともに、データが正しく読み込まれたことをアサートする必要があります。データが表示されないのはなぜですか?これは、タスクを作成したものの、リポジトリに保存していないためです。

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // This DOES NOT save the task anywhere
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

この FakeTestRepository がありますが、フラグメントの実際のリポジトリを偽のリポジトリに置き換える方法が必要です。次はこれを行います。

このタスクでは、ServiceLocator を使用して、フェイク リポジトリをフラグメントに提供します。これにより、フラグメントとビューモデルの統合テストを記述できるようになります。

ビューモデルやリポジトリに依存関係を提供する必要があった以前のように、ここでコンストラクタ依存性注入を使用することはできません。コンストラクタの依存関係注入では、クラスを構築する必要があります。フラグメントとアクティビティは、構築せず、通常はコンストラクタにアクセスできないクラスの例です。

フラグメントを構築しないため、コンストラクタ依存性注入を使用してリポジトリ テスト ダブル(FakeTestRepository)をフラグメントにスワップすることはできません。代わりに、サービス ロケータ パターンを使用します。サービス ロケータ パターンは、依存関係インジェクションの代替手段です。これには、「サービス ロケータ」と呼ばれるシングルトン クラスを作成することが含まれます。このクラスの目的は、通常のコードとテストコードの両方に依存関係を提供することです。通常のアプリコード(main ソースセット)では、これらの依存関係はすべて通常のアプリの依存関係です。テストでは、依存関係のテスト ダブル バージョンを提供するようにサービス ロケータを変更します。

サービス ロケータを使用していない


サービス ロケータの使用

この Codelab アプリでは、次の操作を行います。

  1. リポジトリを構築して保存できるサービス ロケータ クラスを作成します。デフォルトでは、「通常」のリポジトリが作成されます。
  2. リポジトリが必要な場合はサービス ロケータを使用するようにコードをリファクタリングします。
  3. テストクラスで、サービス ロケータのメソッドを呼び出して、「通常の」リポジトリをテストダブルに置き換えます。

ステップ 1. ServiceLocator を作成する

ServiceLocator クラスを作成しましょう。メインのアプリケーション コードで使用されるため、他のアプリコードとともにメインのソースセットに配置されます。

注: ServiceLocator はシングルトンであるため、クラスには Kotlin の object キーワードを使用します。

  1. main ソースセットの最上位に ServiceLocator.kt ファイルを作成します。
  2. ServiceLocator という object を定義します。
  3. database と repository のインスタンス変数を作成し、両方を null に設定します。
  4. 複数のスレッドで使用される可能性があるため、リポジトリに @Volatile でアノテーションを付けます(@Volatile について詳しくは、こちらをご覧ください)。

コードは次のようになります。

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

}

現時点では、ServiceLocator が TasksRepository を返す方法を知っているだけで十分です。既存の DefaultTasksRepository を返すか、必要に応じて新しい DefaultTasksRepository を作成して返します。

次の関数を定義します。

  1. provideTasksRepository - 既存のリポジトリを指定するか、新しいリポジトリを作成します。このメソッドは、複数のスレッドが実行されている状況で、誤って 2 つのリポジトリ インスタンスを作成しないように、this で synchronized にする必要があります。
  2. createTasksRepository - 新しいリポジトリを作成するコード。createTaskLocalDataSource を呼び出して新しい TasksRemoteDataSource を作成します。
  3. createTaskLocalDataSource - 新しいローカル データソースを作成するコード。createDataBase に発信します。
  4. createDataBase - 新しいデータベースを作成するコード。

完成したコードは次のとおりです。

ServiceLocator.kt

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

    fun provideTasksRepository(context: Context): TasksRepository {
        synchronized(this) {
            return tasksRepository ?: createTasksRepository(context)
        }
    }

    private fun createTasksRepository(context: Context): TasksRepository {
        val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
        tasksRepository = newRepo
        return newRepo
    }

    private fun createTaskLocalDataSource(context: Context): TasksDataSource {
        val database = database ?: createDataBase(context)
        return TasksLocalDataSource(database.taskDao())
    }

    private fun createDataBase(context: Context): ToDoDatabase {
        val result = Room.databaseBuilder(
            context.applicationContext,
            ToDoDatabase::class.java, "Tasks.db"
        ).build()
        database = result
        return result
    }
}

ステップ 2. Application で ServiceLocator を使用する

メインのアプリケーション コード(テストではない)を変更して、リポジトリを 1 か所(ServiceLocator)で作成します。

リポジトリ クラスのインスタンスは 1 つだけ作成することが重要です。これを実現するため、Application クラスでサービス ロケータを使用します。

  1. パッケージ階層の最上位で TodoApplication を開き、リポジトリの val を作成して、ServiceLocator.provideTaskRepository を使用して取得したリポジトリを割り当てます。

TodoApplication.kt

class TodoApplication : Application() {

    val taskRepository: TasksRepository
        get() = ServiceLocator.provideTasksRepository(this)

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) Timber.plant(DebugTree())
    }
}

アプリケーションにリポジトリを作成したので、DefaultTasksRepository の古い getRepository メソッドを削除できます。

  1. DefaultTasksRepository を開き、コンパニオン オブジェクトを削除します。

DefaultTasksRepository.kt

// DELETE THIS COMPANION OBJECT
companion object {
    @Volatile
    private var INSTANCE: DefaultTasksRepository? = null

    fun getRepository(app: Application): DefaultTasksRepository {
        return INSTANCE ?: synchronized(this) {
            val database = Room.databaseBuilder(app,
                ToDoDatabase::class.java, "Tasks.db")
                .build()
            DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                INSTANCE = it
            }
        }
    }
}

getRepository を使用していたすべての場所で、代わりにアプリケーションの taskRepository を使用します。これにより、リポジトリを直接作成するのではなく、ServiceLocator が提供したリポジトリを取得できます。

  1. TaskDetailFragement を開き、クラスの上部にある getRepository の呼び出しを見つけます。
  2. この呼び出しを、TodoApplication からリポジトリを取得する呼び出しに置き換えます。

TaskDetailFragment.kt

// REPLACE this code
private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}

// WITH this code

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}
  1. TasksFragment についても同様にします。

TasksFragment.kt

// REPLACE this code
    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
    }


// WITH this code

    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
    }
  1. StatisticsViewModel と AddEditTaskViewModel の場合、リポジトリを取得するコードを更新して、TodoApplication のリポジトリを使用します。

TasksFragment.kt

// REPLACE this code
    private val tasksRepository = DefaultTasksRepository.getRepository(application)



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

  1. アプリケーション(テストではない)を実行します。

リファクタリングのみを行ったため、アプリは問題なく同じように動作するはずです。

ステップ 3. FakeAndroidTestRepository を作成する

テストソースセットに FakeTestRepository がすでに存在します。デフォルトでは、test と androidTest のソースセット間でテストクラスを共有することはできません。そのため、androidTest ソースセットに FakeTestRepository クラスの複製を作成し、FakeAndroidTestRepository と名付ける必要があります。

  1. androidTest ソースセットを右クリックして、データ パッケージを作成します。もう一度右クリックして、ソース パッケージを作成します。
  2. このソース パッケージに FakeAndroidTestRepository.kt という新しいクラスを作成します。
  3. 次のコードをそのクラスにコピーします。

FakeAndroidTestRepository.kt

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap



class FakeAndroidTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private var shouldReturnError = false

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    fun setReturnError(value: Boolean) {
        shouldReturnError = value
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override suspend fun refreshTask(taskId: String) {
        refreshTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    override fun observeTask(taskId: String): LiveData<Result<Task>> {
        runBlocking { refreshTasks() }
        return observableTasks.map { tasks ->
            when (tasks) {
                is Result.Loading -> Result.Loading
                is Error -> Error(tasks.exception)
                is Success -> {
                    val task = tasks.data.firstOrNull() { it.id == taskId }
                        ?: return@map Error(Exception("Not found"))
                    Success(task)
                }
            }
        }
    }

    override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        tasksServiceData[taskId]?.let {
            return Success(it)
        }
        return Error(Exception("Could not find task"))
    }

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        return Success(tasksServiceData.values.toList())
    }

    override suspend fun saveTask(task: Task) {
        tasksServiceData[task.id] = task
    }

    override suspend fun completeTask(task: Task) {
        val completedTask = Task(task.title, task.description, true, task.id)
        tasksServiceData[task.id] = completedTask
    }

    override suspend fun completeTask(taskId: String) {
        // Not required for the remote data source.
        throw NotImplementedError()
    }

    override suspend fun activateTask(task: Task) {
        val activeTask = Task(task.title, task.description, false, task.id)
        tasksServiceData[task.id] = activeTask
    }

    override suspend fun activateTask(taskId: String) {
        throw NotImplementedError()
    }

    override suspend fun clearCompletedTasks() {
        tasksServiceData = tasksServiceData.filterValues {
            !it.isCompleted
        } as LinkedHashMap<String, Task>
    }

    override suspend fun deleteTask(taskId: String) {
        tasksServiceData.remove(taskId)
        refreshTasks()
    }

    override suspend fun deleteAllTasks() {
        tasksServiceData.clear()
        refreshTasks()
    }

   
    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }
}

ステップ 4. テスト用に ServiceLocator を準備する

それでは、テスト時に ServiceLocator を使用してテストダブルを入れ替えることにしましょう。そのためには、ServiceLocator コードにコードを追加する必要があります。

  1. 開く ServiceLocator.kt。
  2. tasksRepository のセッターを @VisibleForTesting としてマークします。このアノテーションは、セッターがパブリックである理由がテストであることを示す方法です。

ServiceLocator.kt

    @Volatile
    var tasksRepository: TasksRepository? = null
        @VisibleForTesting set

テストを単独で実行する場合でも、テストのグループで実行する場合でも、テストはまったく同じように実行される必要があります。つまり、テストは互いに依存する動作を持たないようにする必要があります(テスト間でオブジェクトを共有しないようにします)。

ServiceLocator はシングルトンであるため、テスト間で誤って共有される可能性があります。これを回避するには、テスト間で ServiceLocator 状態を適切にリセットするメソッドを作成します。

  1. Any 値を持つ lock というインスタンス変数を追加します。

ServiceLocator.kt

private val lock = Any()
  1. resetRepository というテスト専用のメソッドを追加します。このメソッドはデータベースをクリアし、リポジトリとデータベースの両方を null に設定します。

ServiceLocator.kt

    @VisibleForTesting
    fun resetRepository() {
        synchronized(lock) {
            runBlocking {
                TasksRemoteDataSource.deleteAllTasks()
            }
            // Clear all data to avoid test pollution.
            database?.apply {
                clearAllTables()
                close()
            }
            database = null
            tasksRepository = null
        }
    }

ステップ 5. ServiceLocator を使用する

このステップでは、ServiceLocator を使用します。

  1. 開く TaskDetailFragmentTest。
  2. lateinit TasksRepository 変数を宣言します。
  3. 各テストの前に FakeAndroidTestRepository を設定し、各テストの後にクリーンアップするための設定メソッドと破棄メソッドを追加します。

TaskDetailFragmentTest.kt

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }
  1. activeTaskDetails_DisplayedInUi() の関数本体を runBlockingTest でラップします。
  2. フラグメントを起動する前に、リポジトリに activeTask を保存します。
repository.saveTask(activeTask)

最終的なテストは次のコードのようになります。

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }
  1. クラス全体に @ExperimentalCoroutinesApi アノテーションを付けます。

完成したコードは次のようになります。

TaskDetailFragmentTest.kt

@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }


    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

}
  1. activeTaskDetails_DisplayedInUi() テストを実行します。

以前と同様にフラグメントが表示されますが、今回はリポジトリを正しく設定したため、タスク情報が表示されます。


このステップでは、Espresso UI テスト ライブラリを使用して、最初の統合テストを完了します。コードを構造化して、UI のアサーションを含むテストを追加できるようにしました。そのためには、Espresso テスト ライブラリを使用します。

Espresso は以下に役立ちます。

  • ビューを操作します(ボタンのクリック、バーのスライド、画面のスクロールなど)。
  • 特定のビューが画面上にあることや、特定の状態(特定のテキストが含まれている、チェックボックスがオンになっているなど)であることをアサートします。

ステップ 1. Gradle 依存関係に関する注意事項

メインの Espresso 依存関係は、Android プロジェクトにデフォルトで含まれているため、すでに存在します。

app/build.gradle

dependencies {

  // ALREADY in your code
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
   
 // Other dependencies
}

androidx.test.espresso:espresso-core - このコア Espresso 依存関係は、新しい Android プロジェクトを作成するときにデフォルトで含まれます。ほとんどのビューとそれらに対するアクションの基本的なテストコードが含まれています。

ステップ 2. アニメーションをオフにする

Espresso テストは実際のデバイスで実行されるため、本質的にインストルメンテーション テストです。発生する問題の 1 つはアニメーションです。アニメーションが遅延しているときに、ビューが画面上にあるかどうかをテストしようとすると、まだアニメーションが実行中であるため、Espresso が誤ってテストを失敗させることがあります。これにより、Espresso テストが不安定になる可能性があります。

Espresso UI テストでは、アニメーションをオフにすることをおすすめします(テストの実行速度も向上します)。

  1. テストデバイスで、[設定] > [開発者向けオプション] に移動します。
  2. [ウィンドウ アニメ スケール]、[トランジション アニメ スケール]、[Animator 再生時間スケール] の 3 つの設定を無効にします。

ステップ 3. Espresso テストを確認する

Espresso テストを作成する前に、Espresso コードを見てみましょう。

onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))

このステートメントは、ID が task_detail_complete_checkbox のチェックボックス ビューを見つけてクリックし、チェックされていることをアサートします。

Espresso ステートメントのほとんどは、次の 4 つの部分で構成されています。

1. 静的 Espresso メソッド

onView

onView は、Espresso ステートメントを開始する静的 Espresso メソッドの例です。onView は最も一般的なオプションの 1 つですが、onData などの他のオプションもあります。

2. ViewMatcher

withId(R.id.task_detail_title_text)

withId は、ID でビューを取得する ViewMatcher の例です。他にもビュー マッチャーがあります。詳しくは、ドキュメントをご覧ください。

3. ViewAction

perform(click())

ViewAction を受け取る perform メソッド。ViewAction はビューに対して実行できる操作です。この例では、ビューのクリックです。

4. ViewAssertion

check(matches(isChecked()))

ViewAssertion を取る check。ViewAssertion は、ビューに関する何かをチェックまたはアサートします。最もよく使用される ViewAssertion は matches アサーションです。アサーションを終了するには、別の ViewMatcher(この場合は isChecked)を使用します。

Espresso ステートメントで perform と check の両方を常に呼び出す必要はありません。check を使用してアサーションを行うだけのステートメントや、perform を使用して ViewAction を行うだけのステートメントを作成できます。

  1. 開く TaskDetailFragmentTest.kt。
  2. activeTaskDetails_DisplayedInUi テストを更新します。

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
    }

必要に応じて、import ステートメントを次に示します。

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not
  1. // THEN コメントの後の部分はすべて Espresso を使用します。テスト構造と withId の使用を調べ、詳細ページの表示方法に関するアサーションを確認します。
  2. テストを実行し、合格することを確認します。

ステップ 4. 省略可: 独自の Espresso テストを作成する

それでは、自分でテストを書いてみましょう。

  1. completedTaskDetails_DisplayedInUi という名前の新しいテストを作成し、このスケルトン コードをコピーします。

TaskDetailFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
       
        // WHEN - Details fragment launched to display task
        
        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
}
  1. 前のテストを見て、このテストを完了します。
  2. 実行して、テストに合格することを確認します。

完成した completedTaskDetails_DisplayedInUi は次のコードのようになります。

TaskDetailFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
        val completedTask = Task("Completed Task", "AndroidX Rocks", true)
        repository.saveTask(completedTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
    }

この最後のステップでは、モックと呼ばれる別の種類のテスト ダブルとテスト ライブラリ Mockito を使用して、Navigation コンポーネントをテストする方法について説明します。

この Codelab では、フェイクと呼ばれるテスト ダブルを使用しました。フェイクは、テスト ダブルの多くの種類の 1 つです。Navigation コンポーネントのテストには、どのテスト ダブルを使用する必要がありますか?

ナビゲーションの仕組みを考えてみましょう。TasksFragment のタスクの 1 つを押して、タスクの詳細画面に移動することを想像してください。

次に、押されたときにタスクの詳細画面に移動する TasksFragment のコードを示します。

TasksFragment.kt

private fun openTaskDetails(taskId: String) {
    val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
    findNavController().navigate(action)
}


ナビゲーションは、navigate メソッドの呼び出しによって発生します。アサート文を記述する必要がある場合、TaskDetailFragment に移動したかどうかをテストする簡単な方法はありません。ナビゲーションは、TaskDetailFragment の初期化以外に明確な出力や状態の変化をもたらさない複雑なアクションです。

アサートできるのは、navigate メソッドが正しいアクション パラメータで呼び出されたことだけです。これは、モック テスト ダブルがまさに実行することです。つまり、特定のメソッドが呼び出されたかどうかをチェックします。

Mockito は、テスト ダブルを作成するためのフレームワークです。API と名前には「モック」という単語が使用されていますが、これは単にモックを作成するためのものではありません。スタブやスパイを作成することもできます。

Mockito を使用して、navigate メソッドが正しく呼び出されたことをアサートできるモック NavigationController を作成します。

ステップ 1. Gradle の依存関係を追加する

  1. Gradle 依存関係を追加します。

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"

    androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion" 

    androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"



  • org.mockito:mockito-core - Mockito の依存関係です。
  • dexmaker-mockito - Android プロジェクトで Mockito を使用するために必要なライブラリです。Mockito は実行時にクラスを生成する必要があります。Android では、これは dex バイトコードを使用して行われるため、このライブラリを使用すると、Mockito は Android で実行時にオブジェクトを生成できます。
  • androidx.test.espresso:espresso-contrib - このライブラリは、DatePicker や RecyclerView などのより高度なビューのテストコードを含む外部コントリビューション(名前の由来)で構成されています。また、後で説明するアクセシビリティ チェックと CountingIdlingResource というクラスも含まれています。

ステップ 2. TasksFragmentTest を作成する

  1. TasksFragment を開きます。
  2. TasksFragment クラス名を右クリックし、[Generate]、[Test] の順に選択します。androidTest ソースセットでテストを作成します。
  3. このコードを TasksFragmentTest にコピーします。

TasksFragmentTest.kt

@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }

}

このコードは、作成した TaskDetailFragmentTest コードと似ています。FakeAndroidTestRepository を設定して破棄します。ナビゲーション テストを追加して、タスクリストのタスクをクリックしたときに正しい TaskDetailFragment に移動することを確認します。

  1. テスト clickTask_navigateToDetailFragmentOne を追加します。

TasksFragmentTest.kt

    @Test
    fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
        repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
        repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        
    }
  1. Mockito の mock 関数を使用してモックを作成します。

TasksFragmentTest.kt

 val navController = mock(NavController::class.java)

Mockito でモックを作成するには、モックするクラスを渡します。

次に、NavController をフラグメントに関連付ける必要があります。onFragment を使用すると、フラグメント自体でメソッドを呼び出すことができます。

  1. 新しいモックをフラグメントの NavController にします。
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. テキスト「TITLE1」を含む RecyclerView のアイテムをクリックするコードを追加します。
// WHEN - Click on the first list item
        onView(withId(R.id.tasks_list))
            .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
                hasDescendant(withText("TITLE1")), click()))

RecyclerViewActions は espresso-contrib ライブラリの一部で、RecyclerView で Espresso アクションを実行できます。

  1. navigate が正しい引数で呼び出されたことを確認します。
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
    TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")

Mockito の verify メソッドにより、これがモックになります。モックされた navController が特定のメソッド(navigate)をパラメータ(ID が「id1」の actionTasksFragmentToTaskDetailFragment)で呼び出したことを確認できます。

完成したテストは次のようになります。

@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
    repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
    repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

    // GIVEN - On the home screen
    val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
    
                val navController = mock(NavController::class.java)
    scenario.onFragment {
        Navigation.setViewNavController(it.view!!, navController)
    }

    // WHEN - Click on the first list item
    onView(withId(R.id.tasks_list))
        .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
            hasDescendant(withText("TITLE1")), click()))


    // THEN - Verify that we navigate to the first detail screen
    verify(navController).navigate(
        TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
    )
}
  1. テストを実行します。

要約すると、ナビゲーションをテストするには、次の方法があります。

  1. Mockito を使用して NavController モックを作成します。
  2. モックの NavController をフラグメントにアタッチします。
  3. navigate が正しいアクションとパラメータで呼び出されたことを確認します。

ステップ 3. 省略可、clickAddTaskButton_navigateToAddEditFragment を記述します

ナビゲーション テストをご自身で作成できるかどうかを確認するには、このタスクをお試しください。

  1. + FAB をクリックすると AddEditTaskFragment に移動することを確認するテスト clickAddTaskButton_navigateToAddEditFragment を記述します。

回答は以下のとおりです。

TasksFragmentTest.kt

    @Test
    fun clickAddTaskButton_navigateToAddEditFragment() {
        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        val navController = mock(NavController::class.java)
        scenario.onFragment {
            Navigation.setViewNavController(it.view!!, navController)
        }

        // WHEN - Click on the "+" button
        onView(withId(R.id.add_task_fab)).perform(click())

        // THEN - Verify that we navigate to the add screen
        verify(navController).navigate(
            TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
                null, getApplicationContext<Context>().getString(R.string.add_task)
            )
        )
    }

こちらをクリックすると、作成したコードと最終的なコードの差分が表示されます。

この Codelab の完成したコードをダウンロードするには、次の git コマンドを使用します。

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_2


または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。

ZIP をダウンロード

この Codelab では、手動依存関係挿入とサービス ロケータを設定する方法と、Android Kotlin アプリでフェイクとモックを使用する方法について説明しました。注意してください。

  • テストする内容とテスト戦略によって、アプリに実装するテストの種類が決まります。単体テストは、焦点を絞った高速なテストです。統合テストでは、プログラムの各部分間の相互作用を検証します。エンドツーエンド テストは、機能の検証、忠実度の高さ、計測の実施が特徴で、実行に時間がかかることがあります。
  • アプリのアーキテクチャは、テストの難易度に影響します。
  • TDD(テスト駆動型開発)は、最初にテストを作成し、次にテストに合格する機能を作成する戦略です。
  • テスト用にアプリの一部を分離するには、テストダブルを使用します。テストダブルは、テスト用に特別に作成されたクラスのバージョンです。たとえば、データベースやインターネットからデータを取得する処理を偽装します。
  • 依存関係挿入を使用して、実際クラス(リポジトリやネットワーキング レイヤなど)をテストクラスに置き換えます。
  • インストルメンテーション テスト(androidTest)を使用して UI コンポーネントを起動します。
  • たとえば、フラグメントを起動する場合など、コンストラクタの依存性注入を使用できない場合は、サービス ロケータを使用できることがよくあります。サービス ロケータ パターンは、依存関係インジェクションの代替手段です。これには、「サービス ロケータ」と呼ばれるシングルトン クラスを作成することが含まれます。このクラスの目的は、通常のコードとテストコードの両方に依存関係を提供することです。

Udacity コース:

Android デベロッパー ドキュメント:

動画:

その他:

このコースの他の Codelab へのリンクについては、Kotlin を使った高度な Android 開発の Codelab のランディング ページをご覧ください。