この Codelab は、Kotlin を使った高度な Android 開発コースの一部です。Codelab を順番に進めると、このコースを最大限に活用できますが、これは必須ではありません。コースの Codelab はすべて、Kotlin を使った高度な Android 開発の Codelab ランディング ページに記載されています。
はじめに
この 2 つ目のテスト Codelab では、テストダブルについて説明します。Android でテストダブルを使用するタイミングや、依存性注入、サービス ロケータ パターン、ライブラリを使用してテストダブルを実装する方法について学びます。この過程で、次の方法を学習します。
- リポジトリの単体テスト
- フラグメントと ViewModel の統合テスト
- フラグメント ナビゲーションのテスト
前提となる知識
以下について把握しておく必要があります。
- Kotlin プログラミング言語
- 最初の Codelab で取り上げたテストのコンセプト: JUnit、Hamcrest、AndroidX テスト、Robolectric を使用した Android での単体テストの作成と実行、LiveData のテスト
- 次のコア Android Jetpack ライブラリ:
ViewModel
、LiveData
、ナビゲーション コンポーネント - アプリ アーキテクチャ ガイドと Android の基礎 Codelab のパターンに準拠したアプリケーション アーキテクチャ
- Android でのコルーチンの基本
学習内容
- テスト戦略を計画する方法
- テストダブル(フェイクとモック)の作成方法と使用方法
- Android で単体テストと統合テストに手動の依存関係インジェクションを使用する方法
- サービス ロケータ パターンの適用方法
- リポジトリ、フラグメント、ビューモデル、Navigation コンポーネントをテストする方法
次のライブラリとコードのコンセプトを使用します。
演習内容
- テスト ダブルと依存関係の注入を使用して、リポジトリの単体テストを作成する。
- テストダブルと依存関係の注入を使用して、ビューモデルの単体テストを作成します。
- Espresso UI テスト フレームワークを使用して、フラグメントとそのビューモデルの統合テストを作成します。
- Mockito と Espresso を使用してナビゲーション テストを作成します。
この一連の Codelab では、TO-DO Notes アプリを使用します。このアプリでは、完了するタスクを書き留めてリストに表示できます。完了または未完了としてマークしたり、フィルタしたり、削除したりできます。
このアプリは Kotlin で記述されており、いくつかの画面があり、Jetpack コンポーネントを使用し、アプリ アーキテクチャ ガイドのアーキテクチャに沿っています。このアプリのテスト方法を学ぶことで、同じライブラリとアーキテクチャを使用するアプリをテストできるようになります。
コードをダウンロードする
まず、コードをダウンロードします。
または、コードの 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 を使用します。以下の例のいずれかをご存知であれば、このアプリのアーキテクチャも同様です。
- Room とビュー Codelab
- Android Kotlin の基礎トレーニングのコードラボ
- 高度な Android トレーニングの Codelab
- Android Sunflower サンプル
- Kotlin による Android アプリの開発 Udacity トレーニング コース
1 つのレイヤのロジックを深く理解するよりも、アプリの一般的なアーキテクチャを理解する方が重要です。
パッケージの概要は次のとおりです。
パッケージ: | |
| タスクの追加または編集画面: タスクの追加または編集用の UI レイヤコード。 |
| データレイヤー: タスクのデータレイヤーを処理します。データベース、ネットワーク、リポジトリのコードが含まれています。 |
| 統計情報画面: 統計情報画面の UI レイヤコード。 |
| タスクの詳細画面: 単一のタスクの UI レイヤコード。 |
| タスク画面: すべてのタスクのリストの UI レイヤコード。 |
| ユーティリティ クラス: アプリのさまざまな部分で使用される共有クラス(複数の画面で使用されるスワイプ更新レイアウトなど)。 |
データレイヤ(.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 アプリは、特定のアーキテクチャに従っています。
このレッスンでは、上記のアーキテクチャの一部を適切に分離してテストする方法について説明します。
- まず、リポジトリの単体テストを行います。
- 次に、ビューモデルでテスト ダブルを使用します。これは、ビューモデルの単体テストと統合テストに必要です。
- 次に、フラグメントとそのビューモデルの統合テストを作成する方法を学びます。
- 最後に、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 環境をシミュレートすべきか?」などの疑問が生じます。
- ネットワーキング コードなど、コードの一部は実行に時間がかかったり、失敗したりすることがあり、実行時間が長く、不安定なテストが作成されることがあります。
- テストの失敗の原因となったコードを診断するテストの機能が失われる可能性があります。テストでリポジトリ以外のコードのテストが開始される可能性があります。たとえば、依存コード(データベース コードなど)の問題が原因で、「リポジトリ」の単体テストが失敗する可能性があります。
テストダブル
この解決策は、リポジトリをテストする際に、実際のネットワーク コードやデータベース コードを使用せず、テスト ダブルを使用することです。テストダブルは、テスト用に特別に作成されたクラスのバージョンです。テストでクラスの実際のバージョンを置き換えることを目的としています。スタント ダブルがスタントを専門とする俳優で、危険なアクションの際に実際の俳優に代わるのと同様です。
テスト ダブルには次のような種類があります。
偽物 | クラスの「動作する」実装を持つテストダブル。テストには適しているが、本番環境には適していない方法で実装されています。 |
モック | どのメソッドが呼び出されたかを追跡するテストダブル。メソッドが正しく呼び出されたかどうかに応じて、テストに合格または不合格となります。 |
スタブ | ロジックを含まず、プログラムされた値を返すだけのテストダブル。たとえば、 |
ダミー | パラメータとして提供する必要がある場合など、渡されるだけで使用されないテストダブル。 |
Spy | 追加情報も追跡するテストダブル。たとえば、 |
テスト ダブルの詳細については、Testing on the Toilet: Know Your Test Doubles をご覧ください。
Android で最もよく使用されるテストダブルは、フェイクとモックです。
このタスクでは、実際のデータソースから切り離された DefaultTasksRepository
を単体テストするために、FakeDataSource
テストダブルを作成します。
ステップ 1: FakeDataSource クラスを作成する
このステップでは、LocalDataSource
と RemoteDataSource
のテストダブルとなる FakeDataSouce
というクラスを作成します。
- test ソースセットで右クリックし、[New] -> [Package] を選択します。
- ソース パッケージを含むデータ パッケージを作成します。
- data/source パッケージに
FakeDataSource
という新しいクラスを作成します。
ステップ 2: TasksDataSource インターフェースを実装する
新しいクラス FakeDataSource
をテストダブルとして使用できるようにするには、他のデータソースを置き換えることができる必要があります。これらのデータソースは TasksLocalDataSource
と TasksRemoteDataSource
です。
- どちらも
TasksDataSource
インターフェースを実装していることに注意してください。
class TasksLocalDataSource internal constructor(
private val tasksDao: TasksDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }
object TasksRemoteDataSource : TasksDataSource { ... }
FakeDataSource
にTasksDataSource
を実装します。
class FakeDataSource : TasksDataSource {
}
Android Studio は、TasksDataSource
の必須メソッドが実装されていないことを示すエラーを表示します。
- クイック フィックス メニューを使用して、[メンバーを実装] を選択します。
- すべてのメソッドを選択して、[OK] を押します。
ステップ 3: FakeDataSource で getTasks メソッドを実装する
FakeDataSource
は、フェイクと呼ばれる特定の種類のテスト ダブルです。フェイクは、クラスの「動作する」実装を持つテスト ダブルですが、テストには適しているものの本番環境には適していない方法で実装されています。「動作する」実装とは、入力に対して現実的な出力を生成するクラスを意味します。
たとえば、偽のデータソースはネットワークに接続したり、データベースに何かを保存したりしません。代わりに、メモリ内のリストを使用します。タスクを取得または保存するメソッドが期待どおりの結果を返すため、これは「期待どおりに動作」しますが、サーバーやデータベースに保存されないため、この実装を本番環境で使用することはできません。
A: FakeDataSource
- を使用すると、実際のデータベースやネットワークに依存することなく、
DefaultTasksRepository
のコードをテストできます。 - テスト用の「十分な」実装を提供します。
FakeDataSource
コンストラクタを変更して、tasks
というvar
を作成します。これは、デフォルト値が空の可変リストであるMutableList<Task>?
です。
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }
データベースまたはサーバーのレスポンスを「偽装」するタスクのリストです。ここでは、リポジトリの getTasks
メソッドをテストすることを目標とします。これにより、データソースの getTasks
、deleteAllTasks
、saveTask
メソッドが呼び出されます。
これらのメソッドのフェイク バージョンを作成します。
- 書き込み
getTasks
:tasks
がnull
でない場合は、Success
の結果を返します。tasks
がnull
の場合、Error
の結果を返します。 - 書き込み
deleteAllTasks
: ミュータブルなタスクリストをクリアします。 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 でコンストラクタ依存性注入を使用する
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 }
- 依存関係を渡したので、
init
メソッドを削除します。依存関係を作成する必要はなくなりました。 - 古いインスタンス変数も削除します。コンストラクタで定義します。
DefaultTasksRepository.kt
// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
- 最後に、新しいコンストラクタを使用するように
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
をテストできます。
DefaultTasksRepository
クラス名を右クリックして、[Generate]、[Test] の順に選択します。- 画面の指示に沿って、test ソースセットに
DefaultTasksRepositoryTest
を作成します。 - 新しい
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 }
- 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
を使用します。
createRepository
というメソッドを作成し、@Before
アノテーションを付けます。remoteTasks
リストとlocalTasks
リストを使用して、フェイク データソースをインスタンス化します。- 作成した 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
テストを作成しましょう。
- リポジトリの
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 依存関係を追加する必要があります。
testImplementation
を使用して、コルーチンをテストするために必要な依存関係をテスト ソースセットに追加します。
app/build.gradle
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
同期をお忘れなく。
kotlinx-coroutines-test
は、コルーチンをテストするために特別に設計されたコルーチン テスト ライブラリです。テストを実行するには、関数 runBlockingTest
を使用します。これは、コルーチン テスト ライブラリが提供する関数です。コードブロックを受け取り、同期的に即座に実行される特別なコルーチン コンテキストでこのコードブロックを実行します。つまり、アクションは決定論的な順序で発生します。これにより、コルーチンはコルーチンではないかのように実行されます。これはコードのテストを目的としています。
suspend
関数を呼び出す場合は、テストクラスで runBlockingTest
を使用します。runBlockingTest
の仕組みとコルーチンのテスト方法については、このシリーズの次の Codelab で詳しく説明します。
- クラスの上に
@ExperimentalCoroutinesApi
を追加します。これは、クラスで試験運用版のコルーチン API(runBlockingTest
)を使用していることを示すものです。これがないと、警告が表示されます。 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))
}
}
- 新しい
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 サーフェス)を含める必要があります。
DefaultTasksRepository
を開き、クラス名を右クリックします。[Refactor -> Extract -> Interface] を選択します。
- [別のファイルに抽出] を選択します。
- [Extract Interface] ウィンドウで、インターフェース名を
TasksRepository
に変更します。 - [Members to form interface] セクションで、2 つのコンパニオン メンバーと private メソッドを除くすべてのメンバーをチェックします。
- [リファクタリング] をクリックします。新しい
TasksRepository
インターフェースが data/source パッケージに表示されます。
また、DefaultTasksRepository
に TasksRepository
が実装されました。
- アプリ(テストではない)を実行して、すべてが正常に動作することを確認します。
ステップ 2. FakeTestRepository を作成する
インターフェースができたので、DefaultTaskRepository
テストダブルを作成できます。
- テスト ソースセットの data/source で、Kotlin ファイルとクラス
FakeTestRepository.kt
を作成し、TasksRepository
インターフェースから拡張します。
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}
インターフェース メソッドを実装する必要があることが通知されます。
- エラーにカーソルを合わせ、候補メニューが表示されたら、クリックして [Implement members] を選択します。
- すべてのメソッドを選択して、[OK] を押します。
ステップ 3. FakeTestRepository メソッドを実装する
これで、「未実装」のメソッドを含む FakeTestRepository
クラスが作成されました。FakeDataSource
を実装したときと同様に、FakeTestRepository
はローカル データソースとリモート データソース間の複雑なメディエーションを処理するのではなく、データ構造によってサポートされます。
FakeTestRepository
は FakeDataSource
などを利用する必要はありません。入力に対して現実的な偽の出力を返すだけで十分です。LinkedHashMap
を使用してタスクのリストを保存し、MutableLiveData
を使用してオブザーバブル タスクを保存します。
FakeTestRepository
で、現在のタスクのリストを表すLinkedHashMap
変数と、オブザーバブル タスクのMutableLiveData
の両方を追加します。
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
// Rest of class
}
次のメソッドを実装します。
getTasks
- このメソッドは、tasksServiceData
を受け取り、tasksServiceData.values.toList()
を使用してリストに変換し、Success
の結果として返します。refreshTasks
-observableTasks
の値をgetTasks()
から返された値に更新します。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
を複数回呼び出すこともできますが、これを簡単にするために、タスクを追加できるテスト専用のヘルパー メソッドを追加します。
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
画面に関連するクラスとテストを更新します。
- 開く
TasksViewModel
。 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
と同じファイルに配置しますが、独自のファイルに配置することもできます。
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
の構築方法を変更する標準的な方法です。ファクトリが用意できたら、ビューモデルを構築する場所でファクトリを使用します。
- ファクトリーを使用するように
TasksFragment
を更新します。
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TasksViewModel>()
// WITH
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
- アプリのコードを実行して、すべてが正常に機能していることを確認します。
ステップ 2. TasksViewModelTest 内で FakeTestRepository を使用する
これで、ビューモデルのテストで実際のリポジトリの代わりに、フェイク リポジトリを使用できるようになりました。
TasksViewModelTest
を開きます。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
}
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)
}
- AndroidX Test
ApplicationProvider.getApplicationContext
コードを使用しなくなったため、@RunWith(AndroidJUnit4::class)
アノテーションも削除できます。 - テストを実行して、すべてが正常に機能することを確認します。
コンストラクタの依存関係の挿入を使用することで、DefaultTasksRepository
を依存関係として削除し、テストで FakeTestRepository
に置き換えました。
ステップ 3. TaskDetail フラグメントと ViewModel も更新する
TaskDetailFragment
と TaskDetailViewModel
にもまったく同じ変更を加えます。これにより、次に TaskDetail
テストを作成する際にコードを準備できます。
- 開く
TaskDetailViewModel
。 - コンストラクタを更新します。
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 }
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)
}
- ファクトリーを使用するように
TasksFragment
を更新します。
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()
// WITH
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
- コードを実行して、すべてが正常に機能することを確認します。
TasksFragment
と TasksDetailFragment
で、実際のリポジトリの代わりに FakeTestRepository
を使用できるようになりました。
次に、フラグメントとビューモデルのインタラクションをテストする統合テストを作成します。ビューモデルのコードが UI を適切に更新するかどうかを確認できます。これを行うには、
- ServiceLocator パターン
- Espresso ライブラリと Mockito ライブラリ
統合テスト では、複数のクラスの相互作用をテストして、それらを一緒に使用したときに想定どおりに動作することを確認します。これらのテストは、ローカル(test
ソースセット)またはインストルメンテーション テスト(androidTest
ソースセット)として実行できます。
この場合、各フラグメントを取得し、フラグメントとビューモデルの統合テストを記述して、フラグメントの主な機能をテストします。
ステップ 1. Gradle の依存関係を追加する
- 次の 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
のフラグメント テストから作成します。
- 開く
taskdetail.TaskDetailFragment
。 - これまでと同様に、
TaskDetailFragment
のテストを生成します。デフォルトの選択をそのまま使用して、androidTest ソースセット(test
ソースセットではない)に配置します。
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
を作成します。
- このテストを
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
を作成します。
このテストはまだ完了していません。何もアサートしていないためです。ここでは、テストを実行して何が起こるかを確認します。
- これはインストルメンテーション テストなので、エミュレータまたはデバイスが表示されていることを確認してください。
- テストを実行します。
次のようになります。
- まず、これはインストルメンテーション テストであるため、テストは物理デバイス(接続されている場合)またはエミュレータで実行されます。
- フラグメントが起動します。
- 他のフラグメントをナビゲートしたり、アクティビティに関連付けられたメニューを表示したりしないことに注目してください。これは、単なるフラグメントです。
最後に、フラグメントがタスクデータを正常に読み込んでいないため、「データなし」と表示されていることを確認します。
テストでは、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. ServiceLocator を作成する
ServiceLocator
クラスを作成しましょう。メインのアプリケーション コードで使用されるため、他のアプリコードとともにメインのソースセットに配置されます。
注: ServiceLocator
はシングルトンであるため、クラスには Kotlin の object
キーワードを使用します。
- main ソースセットの最上位に ServiceLocator.kt ファイルを作成します。
ServiceLocator
というobject
を定義します。database
とrepository
のインスタンス変数を作成し、両方をnull
に設定します。- 複数のスレッドで使用される可能性があるため、リポジトリに
@Volatile
でアノテーションを付けます(@Volatile
について詳しくは、こちらをご覧ください)。
コードは次のようになります。
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
}
現時点では、ServiceLocator
が TasksRepository
を返す方法を知っているだけで十分です。既存の DefaultTasksRepository
を返すか、必要に応じて新しい DefaultTasksRepository
を作成して返します。
次の関数を定義します。
provideTasksRepository
- 既存のリポジトリを指定するか、新しいリポジトリを作成します。このメソッドは、複数のスレッドが実行されている状況で、誤って 2 つのリポジトリ インスタンスを作成しないように、this
でsynchronized
にする必要があります。createTasksRepository
- 新しいリポジトリを作成するコード。createTaskLocalDataSource
を呼び出して新しいTasksRemoteDataSource
を作成します。createTaskLocalDataSource
- 新しいローカル データソースを作成するコード。createDataBase
に発信します。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 クラスでサービス ロケータを使用します。
- パッケージ階層の最上位で
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
メソッドを削除できます。
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
が提供したリポジトリを取得できます。
TaskDetailFragement
を開き、クラスの上部にあるgetRepository
の呼び出しを見つけます。- この呼び出しを、
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)
}
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)
}
StatisticsViewModel
とAddEditTaskViewModel
の場合、リポジトリを取得するコードを更新して、TodoApplication
のリポジトリを使用します。
TasksFragment.kt
// REPLACE this code
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// WITH this code
private val tasksRepository = (application as TodoApplication).taskRepository
- アプリケーション(テストではない)を実行します。
リファクタリングのみを行ったため、アプリは問題なく同じように動作するはずです。
ステップ 3. FakeAndroidTestRepository を作成する
テストソースセットに FakeTestRepository
がすでに存在します。デフォルトでは、test
と androidTest
のソースセット間でテストクラスを共有することはできません。そのため、androidTest
ソースセットに FakeTestRepository
クラスの複製を作成し、FakeAndroidTestRepository
と名付ける必要があります。
androidTest
ソースセットを右クリックして、データ パッケージを作成します。もう一度右クリックして、ソース パッケージを作成します。- このソース パッケージに
FakeAndroidTestRepository.kt
という新しいクラスを作成します。 - 次のコードをそのクラスにコピーします。
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
コードにコードを追加する必要があります。
- 開く
ServiceLocator.kt
。 tasksRepository
のセッターを@VisibleForTesting
としてマークします。このアノテーションは、セッターがパブリックである理由がテストであることを示す方法です。
ServiceLocator.kt
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting set
テストを単独で実行する場合でも、テストのグループで実行する場合でも、テストはまったく同じように実行される必要があります。つまり、テストは互いに依存する動作を持たないようにする必要があります(テスト間でオブジェクトを共有しないようにします)。
ServiceLocator
はシングルトンであるため、テスト間で誤って共有される可能性があります。これを回避するには、テスト間で ServiceLocator
状態を適切にリセットするメソッドを作成します。
Any
値を持つlock
というインスタンス変数を追加します。
ServiceLocator.kt
private val lock = Any()
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
を使用します。
- 開く
TaskDetailFragmentTest
。 lateinit TasksRepository
変数を宣言します。- 各テストの前に
FakeAndroidTestRepository
を設定し、各テストの後にクリーンアップするための設定メソッドと破棄メソッドを追加します。
TaskDetailFragmentTest.kt
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
activeTaskDetails_DisplayedInUi()
の関数本体をrunBlockingTest
でラップします。- フラグメントを起動する前に、リポジトリに
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)
}
- クラス全体に
@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)
}
}
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 テストでは、アニメーションをオフにすることをおすすめします(テストの実行速度も向上します)。
- テストデバイスで、[設定] > [開発者向けオプション] に移動します。
- [ウィンドウ アニメ スケール]、[トランジション アニメ スケール]、[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 つの部分で構成されています。
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
はビューに対して実行できる操作です。この例では、ビューのクリックです。
check(matches(isChecked()))
ViewAssertion
を取る check
。ViewAssertion
は、ビューに関する何かをチェックまたはアサートします。最もよく使用される ViewAssertion
は matches
アサーションです。アサーションを終了するには、別の ViewMatcher
(この場合は isChecked
)を使用します。
Espresso ステートメントで perform
と check
の両方を常に呼び出す必要はありません。check
を使用してアサーションを行うだけのステートメントや、perform
を使用して ViewAction
を行うだけのステートメントを作成できます。
- 開く
TaskDetailFragmentTest.kt
。 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
// THEN
コメントの後の部分はすべて Espresso を使用します。テスト構造とwithId
の使用を調べ、詳細ページの表示方法に関するアサーションを確認します。- テストを実行し、合格することを確認します。
ステップ 4. 省略可: 独自の Espresso テストを作成する
それでは、自分でテストを書いてみましょう。
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
}
- 前のテストを見て、このテストを完了します。
- 実行して、テストに合格することを確認します。
完成した 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 の依存関係を追加する
- 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 を作成する
TasksFragment
を開きます。TasksFragment
クラス名を右クリックし、[Generate]、[Test] の順に選択します。androidTest ソースセットでテストを作成します。- このコードを
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
に移動することを確認します。
- テスト
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)
}
- Mockito の
mock
関数を使用してモックを作成します。
TasksFragmentTest.kt
val navController = mock(NavController::class.java)
Mockito でモックを作成するには、モックするクラスを渡します。
次に、NavController
をフラグメントに関連付ける必要があります。onFragment
を使用すると、フラグメント自体でメソッドを呼び出すことができます。
- 新しいモックをフラグメントの
NavController
にします。
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
- テキスト「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 アクションを実行できます。
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")
)
}
- テストを実行します。
要約すると、ナビゲーションをテストするには、次の方法があります。
- Mockito を使用して
NavController
モックを作成します。 - モックの
NavController
をフラグメントにアタッチします。 - navigate が正しいアクションとパラメータで呼び出されたことを確認します。
ステップ 3. 省略可、clickAddTaskButton_navigateToAddEditFragment を記述します
ナビゲーション テストをご自身で作成できるかどうかを確認するには、このタスクをお試しください。
- + 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 で開くこともできます。
この Codelab では、手動依存関係挿入とサービス ロケータを設定する方法と、Android Kotlin アプリでフェイクとモックを使用する方法について説明しました。注意してください。
- テストする内容とテスト戦略によって、アプリに実装するテストの種類が決まります。単体テストは、焦点を絞った高速なテストです。統合テストでは、プログラムの各部分間の相互作用を検証します。エンドツーエンド テストは、機能の検証、忠実度の高さ、計測の実施が特徴で、実行に時間がかかることがあります。
- アプリのアーキテクチャは、テストの難易度に影響します。
- TDD(テスト駆動型開発)は、最初にテストを作成し、次にテストに合格する機能を作成する戦略です。
- テスト用にアプリの一部を分離するには、テストダブルを使用します。テストダブルは、テスト用に特別に作成されたクラスのバージョンです。たとえば、データベースやインターネットからデータを取得する処理を偽装します。
- 依存関係挿入を使用して、実際クラス(リポジトリやネットワーキング レイヤなど)をテストクラスに置き換えます。
- インストルメンテーション テスト(
androidTest
)を使用して UI コンポーネントを起動します。 - たとえば、フラグメントを起動する場合など、コンストラクタの依存性注入を使用できない場合は、サービス ロケータを使用できることがよくあります。サービス ロケータ パターンは、依存関係インジェクションの代替手段です。これには、「サービス ロケータ」と呼ばれるシングルトン クラスを作成することが含まれます。このクラスの目的は、通常のコードとテストコードの両方に依存関係を提供することです。
Udacity コース:
Android デベロッパー ドキュメント:
- アプリ アーキテクチャ ガイド
runBlocking
、runBlockingTest
FragmentScenario
- Espresso
- Mockito
- JUnit4
- AndroidX テスト ライブラリ
- AndroidX アーキテクチャ コンポーネント コアテスト ライブラリ
- ソースセット
- コマンドラインからテストする
動画:
その他:
このコースの他の Codelab へのリンクについては、Kotlin を使った高度な Android 開発の Codelab のランディング ページをご覧ください。