この Codelab は、Kotlin での高度な Android 開発コースの一部です。Codelab を順番に進めていくと、このコースを最大限に活用できますが、これは必須ではありません。すべてのコース Codelab は Kotlin での Codelab の高度な Codelab のランディング ページに掲載されています。
はじめに
この 2 つ目のテスト Codelab では、Android でテストダブルを使用するタイミングと、依存関係注入、サービス ロケータ パターン、ライブラリを使用して実装する方法について説明します。ここでは、次の方法について学びます。
- リポジトリの単体テスト
- フラグメントとビューモデルの統合テスト
- フラグメント ナビゲーション テスト
前提となる知識
以下について把握しておく必要があります。
- Kotlin プログラミング言語
- 1 つ目の Codelab で説明したテストのコンセプト: JUnit、Hamcrest、AndroidX テスト、Robolectric を使用したテスト、Android での単体テストの実行、LiveData のテスト
- コア Android Jetpack ライブラリ:
ViewModel
、LiveData
、Navigation コンポーネント - アプリケーション アーキテクチャ。アプリ アーキテクチャ ガイドと Android の基礎の Codelab のパターンに従います。
- Android でのコルーチンの基本
学習内容
- テスト戦略を計画する方法
- テストダブルを作成して使用する方法(疑似とモック)
- 単体テストと統合テストに Android で手動依存関係注入を使用する方法
- サービス ロケータ パターンの適用方法
- リポジトリ、フラグメント、ビューモデル、Navigation コンポーネントをテストする方法
以下のライブラリとコードのコンセプトを使用します。
runBlocking
およびrunBlockingTest
FragmentScenario
- Espresso
- モキト
演習内容
- テストダブルと依存関係注入を使用して、リポジトリの単体テストを作成します。
- テストダブルと依存関係注入を使用して、ビューモデルの単体テストを作成します。
- Espresso UI テスト フレームワークを使用して、フラグメントとそのビューモデルの統合テストを作成します。
- Mockito と Espresso を使用してナビゲーション テストを作成します。
このシリーズの Codelab では、ToDo リスト アプリを使用します。このアプリを使用すると、完了すべきタスクを書き留めて、リストで確認できます。その後、完了としてマーク、フィルタ、削除することができます。
このアプリは Kotlin で作成されています。いくつかの画面があり、Jetpack コンポーネントを使用し、アプリ アーキテクチャ ガイドのアーキテクチャに従います。このアプリをテストする方法を学ぶことで、同じライブラリとアーキテクチャを使用するアプリをテストできるようになります。
コードをダウンロードする
まず、コードをダウンロードします。
または、コードの 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 を使用する。下記の例のいずれかに精通している場合は、このアプリのアーキテクチャは類似しています。
- Room とビュー Codelab
- Android Kotlin の基礎トレーニング Codelab
- 高度な Android トレーニング Codelab
- Android Sunflower サンプル
- Kotlin Udacity による Android アプリの開発トレーニング コース
どのレイヤでも、ロジックについて深く理解することよりも、アプリの一般的なアーキテクチャを理解することが重要です。
表示されるパッケージの概要は次のとおりです。
パッケージ: | |
| タスクの追加または編集画面: タスクを追加または編集するための UI レイヤコードです。 |
| データレイヤー: タスクのデータレイヤーに適用されます。これには、データベース、ネットワーク、リポジトリのコードが含まれます。 |
| 統計情報の画面: 統計情報画面の UI レイヤコードです。 |
| タスク詳細画面: 単一タスクの UI レイヤコード。 |
| タスク画面: すべてのタスクのリストの UI レイヤコード。 |
| ユーティリティ クラス: アプリのさまざまな部分で使用される共有クラス(複数の画面で使用されるスワイプ更新レイアウトなど)。 |
データレイヤー(.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 アプリは、特定のアーキテクチャに従います。
このレッスンでは、上記のアーキテクチャの一部を適切に分離してテストする方法について説明します。
- まず、リポジトリの単体テストを行います。
- 次に、ビューモデルでテストを 2 回使用します。これは、ビューモデルの単体テストと統合テストで必要になります。
- 次に、フラグメントとそのビューモデルの統合テストを作成します。
- 最後に、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 シミュレーション環境を取得するかどうか、などの質問が表示されます。
- ネットワーク コードなどのコードの一部では、実行に長い時間がかかることもあれば、失敗して実行時間が長く、不安定なテストになることもあります。
- テストでは、テストの失敗の原因となっているコードを診断できなくなる場合があります。テストでは、リポジトリ以外のコードのテストが開始される場合があります。たとえば、想定しているリポジトリの単体テストが、データベース コードなどの依存コードの問題によって失敗することがあります。
テストダブル
この問題を解決するには、リポジトリをテストする際は、実際のネットワークやデータベースのコードを使用せず、代わりにテストを 2 回行います。テストダブルは、テスト用に作成されたクラスです。これは、テストでクラスの実際のバージョンを置き換えることを目的としています。スタント ダブルはスタントを専門にし、実際のアクションは危険なアクションに取って代わるアクターのようなものです。
テストダブルには次の種類があります。
フェイク | 「動作中」の実装がテストの実装であるが、テストには適していますが本番環境には適さない方法で実装されている。 |
モック | どのメソッドが呼び出されたかを追跡するテストダブル。次に、テストのメソッドが正しく呼び出されたかどうかによって、テストに合格するか失敗します。 |
スタブ | ロジックを含まず、プログラムした対象のみを返すテストダブル。 |
ダミー | 渡されるだけで使用されていないテストの double(パラメータとして提供する必要があるだけの場合など)。 |
スパイ | 追加情報もトラッキングする Test Double。たとえば、 |
テストダブルの詳細については、トイレでテストする: テストダブルを確認するをご覧ください。
Android で使用される最も一般的なテストダブルは、疑似とモックです。
このタスクでは、実際のデータソースから分離した DefaultTasksRepository
の単体テストを作成する FakeDataSource
テストを作成します。
ステップ 1: FakeDataSource クラスを作成する
このステップでは、FakeDataSouce
というクラスを作成します。これは、LocalDataSource
と RemoteDataSource
の倍量のテストになります。
- [test] ソースセットで、[New -> Package] を右クリックします。
- source パッケージを含む data パッケージを作成します。
- 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
は疑似と呼ばれる特定のタイプのテストダブルです。偽物とは、クラスの「実装」を実装したテストダブルですが、テストには適していますが本番環境には適さない方法で実装されます。「動作」とは、クラスが入力に基づいて現実的な結果を生成することを意味します。
たとえば、疑似データソースはネットワークに接続したりデータベースに保存したりせず、代わりにメモリ内リストを使用します。これは想定どおりに動作します。タスクを取得または保存するメソッドを使用すると、期待どおりの結果が得られますが、この実装はサーバーやデータベースに保存されないため、本番環境では使用できません。
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
これは、実際のローカル データソースとリモート データソースの仕組みに似ています。
このステップでは、手動依存関係注入と呼ばれる手法を使用して、作成した疑似テスト double を使用します。
主な問題は、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 }
- 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
を使用します。
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
関数を使用します。これは、コルーチンのテスト ライブラリが提供する関数です。コードブロックを受け取り、このコードブロックを、同期的かつ即時で実行される特別なコルーチン コンテキストで実行します。つまり、アクションは確定的な順序で実行されます。基本的に、コルーチンはコルーチン以外と同様に実行されるので、コードのテストを目的としています。
テストクラスで runBlockingTest
は、suspend
関数の呼び出し時に使用します。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
テストを実行し、エラーが解消されていることを確認します。
リポジトリの単体テストを行う方法を確認しました。次のステップでは、依存関係の挿入を再び使用して、別のテストをもう 1 つ作成します。今回は、ビューモデルの単体テストと統合テストを作成する方法を紹介します。
単体テストでは、関心のあるクラスまたはメソッドのみをテストしてください。これは「分離」というテストと呼ばれ、その「ユニット」に含まれるコードだけを明確に分離します。
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
に変更します。 - [メンバーになるフォームのインターフェース] セクションで、2 つのコンパニオン メンバーと private メソッドを除くすべてのメンバーを確認します。
- [リファクタリング] をクリックします。新しい
TasksRepository
インターフェースが data/source パッケージに表示されます。
DefaultTasksRepository
に TasksRepository
が実装されました。
- (テストではなく)アプリを実行して、すべてが正常に動作していることを確認します。
ステップ 2: FakeTestRepository を作成する
これでインターフェースが完成したので、DefaultTaskRepository
テストダブルを作成できます。
- テストのソースセットで、data/source に Kotlin ファイルとクラス
FakeTestRepository.kt
を作成し、TasksRepository
インターフェースから拡張します。
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}
インターフェース メソッドを実装するよう求められます。
- エラーにカーソルを合わせて提案メニューを表示し、[メンバーを実装する] をクリックします。
- すべての方法を選択し、[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
を数回呼び出すこともできますが、これを簡単にするために、タスクを追加できるテスト専用のヘルパー メソッドを追加します。
- タスクの
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
画面に関連するクラスとテストを更新します。
TasksViewModel
を開きます。- クラス内で構築するのではなく、
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
と同じファイルに含めますが、独自のファイルに配置することもできます。
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
には、1 つのタスクに関する情報が表示されます。
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
を作成します。
まだアサーションを行っていないため、終了したテストはまだ完了していません。ここでは、テストを実行して何が起こるかを確認します。
- これはインストゥルメント化テストであるため、エミュレータまたはデバイスが表示されていることを確認してください。
- テストを実行します。
いくつかの処理が行われます。
- まず、これはインストルメンテーション テストであるため、テストは実機(接続されている場合)またはエミュレータで実行されます。
- フラグメントが起動します。
- 他のフラグメント内を移動したり、アクティビティに関連付けられたメニューを持たせたりせず、フラグメントだけに注意してください。
最後に、タスクデータをロードできないため、フラグメントが「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 アプリでは、次の操作を行います。
- リポジトリを構築して保存できる Service Locator クラスを作成します。デフォルトでは、「normal」リポジトリが作成されます。
- リポジトリが必要な場合は、サービス ロケータを使用するようにコードをリファクタリングします。
- テストクラスで、サービス ロケータのメソッドを呼び出し、「normal」リポジトリをテストに入れ替えます。
ステップ 1: ServiceLocator を作成する
ServiceLocator
クラスを作成しましょう。メイン アプリコードで使用されるため、アプリの他の部分と一緒にメイン ソースセットに配置されます。
注: ServiceLocator
はシングルトンなので、クラスに Kotlin object
キーワードを使用します。
- メイン ソースセットの最上位に 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: アプリケーションで ServiceLocator を使用する
ここでは、テストではなくメイン アプリケーション コードに変更を加え、1 か所で ServiceLocator
というリポジトリを作成します。
リポジトリ クラスのインスタンスは 1 つだけにすることが重要になります。これを確認するには、Application クラスの Service locator を使用します。
- パッケージ階層の最上位で、
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
ソースセットを右クリックして、data パッケージを作成します。右クリックして、ソースパッケージを作成します。- このソース パッケージ内に
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
を使用してスワップする時間が 2 倍になりました。これを行うには、ServiceLocator
コードになんらかのコードを追加する必要があります。
ServiceLocator.kt
を開きます。tasksRepository
のセッターを@VisibleForTesting
としてマークします。このアノテーションは、セッターが公開されている理由はテストによるものであることを表現する方法です。
ServiceLocator.kt
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting set
テストを単独で実行する場合も、一連のテストで実行する場合も、まったく同じようにテストを行う必要があります。つまり、テストが相互に依存する動作(テスト間でオブジェクトを共有しない)があってはなりません。
ServiceLocator
はシングルトンなので、テスト間で誤って共有される可能性があります。これを回避するには、テスト間の ServiceLocator
状態を適切にリセットするメソッドを作成します。
lock
というインスタンス変数をAny
値で追加します。
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 プロジェクトにデフォルトで含まれているため、メインの 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 テストでは、アニメーションをオフにすることをおすすめします(テストの実行も速くなります)。
- テストデバイスで [設定] > [開発者向けオプション] に移動します。
- [ウィンドウ アニメーションのスケール]、[トランジションのアニメーションのスケール]、[アニメーターの再生時間のスケール] の 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
は、最も一般的なオプションの一つですが、onData
など、他のオプションもあります。
2. ビューマッチャー
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
内のいずれかのタスクを押して、タスクの詳細画面に移動するとします。
押すとタスクの詳細画面に移動する 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 依存関係を追加する
- 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
メソッドは、モック化を行います。メソッド(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")
)
}
- テストを実行します。
要約すると、ナビゲーションをテストする手順は次のとおりです。
- Mockito を使用して
NavController
モックを作成します。 - モックした
NavController
をフラグメントにアタッチします。 - ナビゲーションが適切なアクションとパラメータで呼び出されたことを確認します。
ステップ 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
- モキト
- JUnit4
- AndroidX テスト ライブラリ
- AndroidX アーキテクチャ コンポーネントのコアテスト ライブラリ
- ソースセット
- コマンドラインからテストする
動画:
その他:
このコースの他の Codelab へのリンクについては、Kotlin Codelab の高度な Codelab のランディング ページをご覧ください。