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

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

はじめに

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

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

前提となる知識

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

学習内容

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

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

演習内容

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

このシリーズの Codelab では、ToDo リスト アプリを使用します。このアプリを使用すると、完了すべきタスクを書き留めて、リストで確認できます。その後、完了としてマーク、フィルタ、削除することができます。

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

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

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

ZIP をダウンロード

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

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

以下の手順に沿って、コードの内容をよくご確認ください。

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

ToDo アプリをダウンロードしたら、Android Studio で開いて実行します。コンパイルできるはずです。アプリを探索する手順は次のとおりです。

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

ステップ 2: サンプルアプリのコードを調べる

TO-DO アプリは、一般的なアーキテクチャ ブループリントのテストとアーキテクチャのサンプル(リアクティブ アーキテクチャ バージョンのサンプル)に基づいています。アプリは、アプリ アーキテクチャ ガイドのアーキテクチャに沿って実行されます。Fragment、リポジトリ、Room とともに ViewModel を使用する。下記の例のいずれかに精通している場合は、このアプリのアーキテクチャは類似しています。

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

表示されるパッケージの概要は次のとおりです。

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

.addedittask

タスクの追加または編集画面: タスクを追加または編集するための UI レイヤコードです。

.data

データレイヤー: タスクのデータレイヤーに適用されます。これには、データベース、ネットワーク、リポジトリのコードが含まれます。

.statistics

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

.taskdetail

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

.tasks

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

.util

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

データレイヤー(.data)

このアプリには、シミュレートされたネットワーキング レイヤ(リモート パッケージ)とデータベース レイヤ(ローカル パッケージ)が含まれています。わかりやすくするため、このプロジェクトでは、実際のネットワーク リクエストを作成するのではなく、遅延を考慮して HashMap のみでネットワーク レイヤをシミュレートします。

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

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

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

ナビゲーション

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

この Codelab では、テストの double と依存関係の挿入を使用して、リポジトリのテスト、モデル、フラグメントの表示を行う方法について説明します。テストの説明に入る前に、その理由とテストを作成するにあたって役立つ内容について理解しておくことが重要です。

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

テストのピラミッド

テスト戦略について考える際は、次の 3 つのテスト項目を参考にしてください。

  • スコープ - テストで処理するコードの量。テストは、単一の方法、アプリケーション全体、またはその中間で実行することができます。
  • 速度 - テストの実行速度。テストの速度はミリ秒から数分までさまざまです。
  • 忠実度 - テストの内容たとえば、テストしているコードの一部がネットワーク リクエストを行う必要がある場合、テストコードは実際にこのネットワーク リクエストを行いますか、それとも結果が偽装されますか?テストが実際にネットワークと通信する場合、テストの再現性は高くなります。そのトレードオフとして、テストの実行に時間がかかること、ネットワークがダウンした場合にエラーが発生すること、コストがかかる可能性があります。

これらの要素間には固有のトレードオフがあります。たとえば、速度と忠実度はトレードオフです。テストが速いほど、一般に忠実度は低くなります。その逆も同様です。自動テストは、次の 3 つのカテゴリに分類されます。

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

多くの場合、テストの割合はピラミッドで表され、テストの大部分が単体テストです。

アーキテクチャとテスト

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

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



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

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

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

単体テストをクラスの一部(メソッドまたは少数のメソッド コレクション)について記述する場合、目標はクラスのコードのみをテストすることです。

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

目的は、このクラスのコードのみをテストすることです。ただし、DefaultTaskRepositoryLocalTaskDataSourceRemoteTaskDataSource などの他のクラスに依存して機能します。言い換えると、LocalTaskDataSourceRemoteTaskDataSourceDefaultTaskRepository の依存関係であるということです。

したがって、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 シミュレーション環境を取得するかどうか、などの質問が表示されます。
  • ネットワーク コードなどのコードの一部では、実行に長い時間がかかることもあれば、失敗して実行時間が長く、不安定なテストになることもあります。
  • テストでは、テストの失敗の原因となっているコードを診断できなくなる場合があります。テストでは、リポジトリ以外のコードのテストが開始される場合があります。たとえば、想定しているリポジトリの単体テストが、データベース コードなどの依存コードの問題によって失敗することがあります。

テストダブル

この問題を解決するには、リポジトリをテストする際は、実際のネットワークやデータベースのコードを使用せず、代わりにテストを 2 回行います。テストダブルは、テスト用に作成されたクラスです。これは、テストでクラスの実際のバージョンを置き換えることを目的としています。スタント ダブルはスタントを専門にし、実際のアクションは危険なアクションに取って代わるアクターのようなものです。

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

フェイク

「動作中」の実装がテストの実装であるが、テストには適していますが本番環境には適さない方法で実装されている。

モック

どのメソッドが呼び出されたかを追跡するテストダブル。次に、テストのメソッドが正しく呼び出されたかどうかによって、テストに合格するか失敗します。

スタブ

ロジックを含まず、プログラムした対象のみを返すテストダブル。StubTaskRepository は、getTasks からの特定のタスクの組み合わせを返すようにプログラムできます。

ダミー

渡されるだけで使用されていないテストの double(パラメータとして提供する必要があるだけの場合など)。NoOpTaskRepository を使用している場合、どのメソッドにもコードがないので TaskRepository が実装されます。

スパイ

追加情報もトラッキングする Test Double。たとえば、SpyTaskRepository を作成した場合は、addTask メソッドが呼び出された回数をトラッキングできます。

テストダブルの詳細については、トイレでテストする: テストダブルを確認するをご覧ください。

Android で使用される最も一般的なテストダブルは、疑似モックです。

このタスクでは、実際のデータソースから分離した DefaultTasksRepository の単体テストを作成する FakeDataSource テストを作成します。

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

このステップでは、FakeDataSouce というクラスを作成します。これは、LocalDataSourceRemoteDataSource の倍量のテストになります。

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

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

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

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

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

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

}

Android Studio が TasksDataSource に必要なメソッドを実装していないというエラーを表示します。

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


  1. すべての方法を選択し、[OK] を押します。

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

FakeDataSource疑似と呼ばれる特定のタイプのテストダブルです。偽物とは、クラスの「実装」を実装したテストダブルですが、テストには適していますが本番環境には適さない方法で実装されます。「動作」とは、クラスが入力に基づいて現実的な結果を生成することを意味します。

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

FakeDataSource

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


これは、偽の(データベースまたはサーバーの応答である)タスクのリストです。現時点では、リポジトリgetTasks メソッドをテストすることが目標です。これにより、データソースgetTasksdeleteAllTaskssaveTask メソッドが呼び出されます。

次のメソッドの疑似バージョンを作成します。

  1. getTasks と記述する: tasksnull でない場合は、Success の結果を返します。tasksnull の場合は、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

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

このステップでは、手動依存関係注入と呼ばれる手法を使用して、作成した疑似テスト double を使用します。

主な問題は、FakeDataSource があるにもかかわらず、テストでの使用方法を明確にしないことです。TasksRemoteDataSourceTasksLocalDataSource を置き換える必要がありますが、テストの場合のみです。TasksRemoteDataSourceTasksLocalDataSource はどちらも DefaultTasksRepository の依存関係です。つまり、DefaultTasksRepositories の実行にはこれらのクラスが依存する、または依存します。

現時点では、依存関係は DefaultTasksRepositoryinit メソッドで作成されます。

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 内で taskLocalDataSourcetasksRemoteDataSource を作成して割り当てるため、これらは基本的にハードコードされます。テストを重複させる方法はありません。

代わりに、ハードコードするのではなく、データソースをクラスに提供します。依存関係の提供は、依存関係注入と呼ばれます。依存関係を指定する方法はいくつかあり、依存関係の挿入の種類が異なります。

コンストラクタの依存関係の依存関係では、テストをコンストラクタに渡すことでダブルスワップできます。

インジェクションなし

インジェクション

ステップ 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. 2 つの FakeDataSource メンバー変数(リポジトリの各データソースに 1 つ)と、テストする DefaultTasksRepository の変数を 3 つ作成します。

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. remoteTaskslocalTasks のリストを使用して、疑似データソースをインスタンス化します。
  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 を追加する

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

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

app/build.gradle

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

必ず同期してください。

kotlinx-coroutines-test は、コルーチンをテストするライブラリです。テストを実行するには、runBlockingTest 関数を使用します。これは、コルーチンのテスト ライブラリが提供する関数です。コードブロックを受け取り、このコードブロックを、同期的かつ即時で実行される特別なコルーチン コンテキストで実行します。つまり、アクションは確定的な順序で実行されます。基本的に、コルーチンはコルーチン以外と同様に実行されるので、コードのテストを目的としています。

テストクラスで runBlockingTest は、suspend 関数の呼び出し時に使用します。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 テストを実行し、エラーが解消されていることを確認します。

リポジトリの単体テストを行う方法を確認しました。次のステップでは、依存関係の挿入を再び使用して、別のテストをもう 1 つ作成します。今回は、ビューモデルの単体テストと統合テストを作成する方法を紹介します。

単体テストでは、関心のあるクラスまたはメソッドのみをテストしてください。これは「分離」というテストと呼ばれ、その「ユニット」に含まれるコードだけを明確に分離します。

TasksViewModelTestTasksViewModel コードのみをテストする必要があります。データベース、ネットワーク、リポジトリ クラスではテストしないでください。したがって、ビューモデルについては、リポジトリの場合と同様に、疑似リポジトリを作成し、テストで使用する依存関係注入を適用します。

このタスクでは、ビューに依存関係注入を適用します。

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

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

実際の状況TasksRemoteDataSourceTasksLocalDataSourceFakeDataSource を見てみると、すべて同じインターフェース「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. [メンバーになるフォームのインターフェース] セクションで、2 つのコンパニオン メンバーと private メソッドを除くすべてのメンバーを確認します。


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

DefaultTasksRepositoryTasksRepository が実装されました。

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

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

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

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

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

インターフェース メソッドを実装するよう求められます。

  1. エラーにカーソルを合わせて提案メニューを表示し、[メンバーを実装する] をクリックします。
  1. すべての方法を選択し、[OK] を押します。

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

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

FakeTestRepositoryFakeDataSource などを使用する必要はありません。必要な場合は、入力に基づいて現実的な疑似出力を返す必要があります。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. タスクの vararg を受け取り、それぞれを HashMap に追加してからタスクを更新する addTasks メソッドを追加します。

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. クラス内で構築するのではなく、TasksRepository を受け取るように TasksViewModel のコンストラクタを変更します。

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 を作成する必要があります。Factory クラスは 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. TasksViewModelTestFakeTestRepository プロパティを追加します。

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 も更新する

TaskDetailFragmentTaskDetailViewModel にまったく同じ変更を加えます。これで、次に 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. コードを実行し、すべてが機能していることを確認します。

TasksFragmentTasksDetailFragment の実際のリポジトリの代わりに 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 には、1 つのタスクに関する情報が表示されます。

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. テストを実行します。

いくつかの処理が行われます。

  • まず、これはインストルメンテーション テストであるため、テストは実機(接続されている場合)またはエミュレータで実行されます。
  • フラグメントが起動します。
  • 他のフラグメント内を移動したり、アクティビティに関連付けられたメニューを持たせたりせず、フラグメントだけに注意してください。

最後に、タスクデータをロードできないため、フラグメントが「No data」と表示されています。

テストでは、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 ソースセット)では、これらの依存関係はすべて通常のアプリ依存関係です。テストでは、Service Locator を変更して、依存関係の二重のバージョンをテストします。

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


サービス ロケータの使用

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

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

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

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

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

  1. メイン ソースセットの最上位に 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: アプリケーションで ServiceLocator を使用する

ここでは、テストではなくメイン アプリケーション コードに変更を加え、1 か所で ServiceLocator というリポジトリを作成します。

リポジトリ クラスのインスタンスは 1 つだけにすることが重要になります。これを確認するには、Application クラスの Service locator を使用します。

  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. StatisticsViewModelAddEditTaskViewModel の場合、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 ソースセットを右クリックして、data パッケージを作成します。右クリックして、ソースパッケージを作成します。
  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 を使用してスワップする時間が 2 倍になりました。これを行うには、ServiceLocator コードになんらかのコードを追加する必要があります。

  1. ServiceLocator.kt を開きます。
  2. tasksRepository のセッターを @VisibleForTesting としてマークします。このアノテーションは、セッターが公開されている理由はテストによるものであることを表現する方法です。

ServiceLocator.kt

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

テストを単独で実行する場合も、一連のテストで実行する場合も、まったく同じようにテストを行う必要があります。つまり、テストが相互に依存する動作(テスト間でオブジェクトを共有しない)があってはなりません。

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

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

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 プロジェクトにデフォルトで含まれているため、メインの Espresso 依存関係はすでに存在します。

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 テストは実機で実行されるため、インストルメンテーション テストに使用します。発生する問題の一つは、アニメーションが遅れて、ビューが画面に表示されているかどうかをテストしようとしたときにまだアニメーション化されている場合です。Espresso が誤ってテストが失敗することがあります。そのため、Espresso テストが不安定になる可能性があります。

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

  1. テストデバイスで [設定] > [開発者向けオプション] に移動します。
  2. [ウィンドウ アニメーションのスケール]、[トランジションのアニメーションのスケール]、[アニメーターの再生時間のスケール] の 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 は、最も一般的なオプションの一つですが、onData など、他のオプションもあります。

2. ビューマッチャー

withId(R.id.task_detail_title_text)

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

3. ViewAction

perform(click())

ViewAction を受け取る perform メソッドViewAction は、ビューをクリックすることで実行できます。たとえば、ここでは、ビューをクリックします。

4. ViewAssertion

check(matches(isChecked()))

ViewAssertion を受け取る checkViewAssertion は、ビューについて何かを確認またはアサートします。最も一般的な ViewAssertion は、matches アサーションです。アサーションを完了するには、別の ViewMatcher(この場合は isChecked)を使用します。

Espresso ステートメントで performcheck の両方を常に呼び出すとは限りません。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 内のいずれかのタスクを押して、タスクの詳細画面に移動するとします。

押すとタスクの詳細画面に移動する TasksFragment のコードです。

TasksFragment.kt

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


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

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

Mockito は、テストを 2 倍にするためのフレームワークです。モックという言葉は API や名前では使用されますが、モックを作るためだけに使用されることはありません。また、スタブやスパイを作成することもあります。

Mockito を使用して、ナビゲーション メソッドが正しく呼び出されたことをアサートできるモック 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 - このライブラリは、DatePickerRecyclerView などの高度なビュー用のテストコードを含む外部寄与(したがって名前)で構成されています。また、ユーザー補助チェックと 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()))

RecyclerViewActionsespresso-contrib ライブラリに含まれており、RecyclerView で Espresso アクションを実行できます。

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

Mockito の verify メソッドは、モック化を行います。メソッド(navigate)と呼ばれるモックされた navController を、ID(ID が「id1」)の特定のメソッド(navigate)で確認できます。

完全なテストは次のようになります。

@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. ナビゲーションが適切なアクションとパラメータで呼び出されたことを確認します。

ステップ 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 Codelab の高度な Codelab のランディング ページをご覧ください。