テストの基本

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

はじめに

初めてアプリ用の機能を実装したとき、コードを実行して意図したとおりに動作していることを確認します。手動テストであっても、テストを実施した。機能の追加や更新を続けて、コードの実行と動作の検証も続けられたのではないでしょうか。しかし、この作業は毎回手間がかかり、間違いが起こりやすいだけでなく、規模拡大にもつながりません。

コンピュータはスケーリングと自動化に長けている!そのため、大小さまざまなデベロッパーが、自動テストを作成します。これは、ソフトウェアによって実行されるテストであり、コードの動作を確認するためにアプリを手動で操作する必要はありません。

この一連の Codelab で学ぶのは、実世界のアプリ用にテストのコレクション(テストスイート)を作成する方法です。

この最初の Codelab では、Android でのテストの基本について説明します。最初のテストを作成し、LiveDataViewModel をテストする方法を学びます。

前提となる知識

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

学習内容

次のトピックについて学びます。

  • Android で単体テストを作成して実行する方法
  • テスト駆動開発の使用方法
  • インストルメンテーション テストとローカルテストの選び方

以下のライブラリとコードのコンセプトについて学びます。

演習内容

  • Android でローカルテストとインストルメンテーション テストの両方を設定、実行、解釈します。
  • JUnit4 と Hamcrest を使用して Android で単体テストを作成します。
  • 単純な LiveData テストと ViewModel テストを作成します。

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

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

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

ZIP をダウンロード

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

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

このタスクでは、アプリを実行してコードベースを確認します。

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

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

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

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

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

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

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

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

.addedittask

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

.data

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

.statistics

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

.taskdetail

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

.tasks

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

.util

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

データレイヤー(.data)

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

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

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

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

ナビゲーション

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

このタスクでは、最初のテストを実行します。

  1. Android Studio で [Project] ペインを開き、次の 3 つのフォルダを見つけます。
  • com.example.android.architecture.blueprints.todoapp
  • com.example.android.architecture.blueprints.todoapp (androidTest)
  • com.example.android.architecture.blueprints.todoapp (test)

これらのフォルダは、ソースセットと呼ばれます。ソースセットとは、アプリのソースコードを含むフォルダです。緑のセット(androidTesttest)のソースセットに、テストが含まれています。新しい Android プロジェクトを作成すると、デフォルトで次の 3 つのソースセットが取得されます。それらは次のとおりです。

  • main: アプリコードが含まれます。このコードは、ビルド可能なさまざまなバージョンのアプリ(ビルド バリアント)で共有されます。
  • androidTest: インストルメンテーション テストと呼ばれるテストが含まれます。
  • test: ローカルテストと呼ばれるテストが含まれます。

ローカルテストとインストルメンテーション テストの違いは、実行方法にあります。

ローカルテスト(test ソースセット)

これらのテストは開発マシンの JVM 上でローカルに実行されるため、エミュレータや物理デバイスは必要ありません。そのため、動作は速くなりますが、忠実度は低くなります。つまり、現実世界と同じように動作しにくくなります。

Android Studio では、ローカルテストは緑と赤の三角形のアイコンで表されます。

インストルメンテーション テスト(androidTest ソースセット)

これらのテストは、実際の Android デバイスまたはエミュレートされた Android デバイスで実行されるため、現実世界で何が起こるかが反映されますが、はるかに遅くなります。

Android Studio のインストルメンテーション テストは、緑色と赤色の三角形のアイコンが付いた Android で表されます。

ステップ 1: ローカルテストを実行する

  1. ExampleUnitTest.kt ファイルが見つかるまで test フォルダを開きます。
  2. 右クリックして、[Run ExampleUnitTest] を選択します。

画面の下部にある [Run] ウィンドウに次の出力が表示されます。

  1. 緑色のチェックマークが表示され、テスト結果を展開して、addition_isCorrect という 1 つのテストに合格したことを確認します。追加機能が意図したとおりに機能することを知っておいてください。

ステップ 2: テストに失敗する

以下は、実行したテストです。

ExampleUnitTest.kt

// A test class is just a normal class
class ExampleUnitTest {

   // Each test is annotated with @Test (this is a Junit annotation)
   @Test
   fun addition_isCorrect() {
       // Here you are checking that 4 is the same as 2+2
       assertEquals(4, 2 + 2)
   }
}

テスト

  • は、テスト ソースセットのいずれかのクラスである。
  • @Test アノテーションで始まる関数が含まれている(各関数は 1 つのテストです)。
  • u8sually には、アサーション ステートメントを含めます。

Android は、テスト ライブラリ JUnit をテストに使用します(この Codelab JUnit4 に含まれています)。アサーションと @Test アノテーションはどちらも JUnit から取得されます。

アサーションはテストの中核です。これは、コードまたはアプリが期待どおりに動作することをチェックするコードステートメントです。この場合のアサーションは assertEquals(4, 2 + 2) であり、4 が 2 + 2 に等しいことを確認します。

失敗したテストの内容を確認するには、簡単に失敗できるアサーションを追加します。3 が 1 + 1 に等しいかどうかをチェックします。

  1. addition_isCorrect テストに assertEquals(3, 1 + 1) を追加します。

ExampleUnitTest.kt

class ExampleUnitTest {

   // Each test is annotated with @Test (this is a Junit annotation)
   @Test
   fun addition_isCorrect() {
       assertEquals(4, 2 + 2)
       assertEquals(3, 1 + 1) // This should fail
   }
}
  1. テストを実行します。
  1. テスト結果では、テストの横に [X] が表示されます。

  1. また、次の点にもご注意ください。
  • アサーションが 1 回失敗すると、テスト全体が失敗します。
  • 期待値(3)と実際に計算された値(2)が示されます。
  • 失敗したアサーションの (ExampleUnitTest.kt:16) の行に移動します。

ステップ 3: インストルメンテーション テストを実行する

インストルメンテーション テストは androidTest ソースセットに含まれています。

  1. androidTest ソースセットを開きます。
  2. ExampleInstrumentedTest というテストを実行します。

ExampleInstrumentedTest

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @Test
    fun useAppContext() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.example.android.architecture.blueprints.reactive",
            appContext.packageName)
    }
}

ローカルテストとは異なり、このテストはデバイス(以下の例では、エミュレートされた Google Pixel 2)で実施します。

デバイスが接続されている場合、またはエミュレータが稼働している場合、エミュレータでテストが実行されます。

このタスクでは、getActiveAndCompleteStats のテストを作成します。このアプリは、アプリのアクティブ タスクと完了済みのタスクの割合(%)を計算します。これらの数値はアプリの統計画面で確認できます。

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

  1. main ソースセットの todoapp.statisticsStatisticsUtils.kt を開きます。
  2. getActiveAndCompletedStats 関数を見つけます。

StatisticsUtils.kt

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

   val totalTasks = tasks!!.size
   val numberOfActiveTasks = tasks.count { it.isActive }
   val activePercent = 100 * numberOfActiveTasks / totalTasks
   val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks

   return StatsResult(
       activeTasksPercent = activePercent.toFloat(),
       completedTasksPercent = completePercent.toFloat()
   )
  
}

data class StatsResult(val activeTasksPercent: Float, val completedTasksPercent: Float)

getActiveAndCompletedStats 関数はタスクのリストを受け入れ、StatsResult を返します。StatsResult は、2 つの数値、完了したタスクの割合、およびアクティブなタスクの割合を含むデータクラスです。

Android Studio には、テスト用のスタブを生成するツールが用意されています。

  1. getActiveAndCompletedStats を右クリックして [Generate &ttest] を選択します。

[Create Test] ダイアログが開きます。

  1. [Class name:] を StatisticsUtilsTest に変更します(StatisticsUtilsKtTest ではなく、テストクラス名に KT は適用しない)。
  2. 他の項目はデフォルト値のままにしておきます。JUnit 4 が適切なテスト ライブラリです。宛先パッケージが正しい(StatisticsUtils クラスの場所を反映している)ため、チェックボックスをチェックする必要はありません(余分なコードが生成されますが、テストはゼロから作成します)。
  3. [OK] を押します。

[Choose Destination Directory] ダイアログが開きます。

関数の計算はローカルで行うため、ローカルテストを作成します。これは Android 固有のコードを含んでいないためです。そのため、実際のデバイスまたはエミュレートしたデバイスで実行する必要はありません。

  1. ローカルテストを作成するため、test ディレクトリ(androidTest ではない)を選択します。
  2. [OK] をクリックします。
  3. 生成された StatisticsUtilsTest クラスが test/statistics/ にあります。

ステップ 2: 最初のテスト関数を作成する

以下を確認するテストを作成します。

  • 完了したタスクとアクティブなタスクが 1 つもない場合、
  • アクティブなテストの割合が 100% であること
  • 完了したタスクの割合は 0% です。
  1. StatisticsUtilsTest を開きます。
  2. getActiveAndCompletedStats_noCompleted_returnsHundredZero という名前の関数を作成します。

StatisticsUtilsTest.kt

class StatisticsUtilsTest {

    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {
        // Create an active task

        // Call your function

        // Check the result
    }
}
  1. 関数名の上に @Test アノテーションを追加して、テストであることを示します。
  2. タスクリストを作成する。
// Create an active task 
val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
  1. これらのタスクで getActiveAndCompletedStats を呼び出します。
// Call your function
val result = getActiveAndCompletedStats(tasks)
  1. アサーションを使用して、result が想定どおりであることを確認します。
// Check the result
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)

完全なコードは次のとおりです。

StatisticsUtilsTest.kt

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {

        // Create an active task (the false makes this active)
        val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
        // Call your function
        val result = getActiveAndCompletedStats(tasks)

        // Check the result
        assertEquals(result.completedTasksPercent, 0f)
        assertEquals(result.activeTasksPercent, 100f)
    }
}
  1. テストを実行します(StatisticsUtilsTest を右クリックし、[Run] を選択します)。

次の条件を満たす必要があります。

ステップ 3: Hamcrest 依存関係を追加する

テストはコードが行うことに関するドキュメントとして機能するため、人が読める形式にすると便利です。次の 2 つのアサーションを比較します。

assertEquals(result.completedTasksPercent, 0f)

// versus

assertThat(result.completedTasksPercent, `is`(0f))

2 番目のアサーションは人間の文によく似ています。Hamcrest というアサーション フレームワークを使用して記述されています。読みやすいアサーションを作成するためのもう 1 つのツールは、Truth ライブラリです。この Codelab では、Hamcrest を使用してアサーションを作成します。

  1. build.grade (Module: app) を開き、次の依存関係を追加します。

app/build.gradle

dependencies {
    // Other dependencies
    testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
}

通常は、依存関係を追加する際に implementation を使用しますが、ここでは testImplementation を使用しています。アプリを公開する準備ができたら、アプリのテストコードや依存関係に APK のサイズを拡大するのは避けましょう。Gradle 設定を使用すると、ライブラリをメインコードに含めるかテストコードに含めるかを指定できます。最も一般的な構成は次のとおりです。

  • implementation - 依存関係は、テスト ソースセットを含むすべてのソースセットで使用可能になります。
  • testImplementation - 依存関係はテスト ソースセットでのみ使用できます。
  • androidTestImplementation - 依存関係は androidTest ソースセットでのみ使用できます。

どの構成を使用するかは、依存関係を使用できる場所を定義します。次のように記述します。

testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"

そのため、Hamcrest はテスト ソースセットでのみ利用可能です。また、最終的なアプリには Hamcrest が含まれなくなります。

ステップ 4: Hamcrest を使用してアサーションを作成する

  1. assertEquals の代わりに Hamcrest の assertThat を使用するように getActiveAndCompletedStats_noCompleted_returnsHundredZero() テストを更新します。
// REPLACE
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)

// WITH
assertThat(result.activeTasksPercent, `is`(100f))
assertThat(result.completedTasksPercent, `is`(0f))

メッセージが表示されたら、インポート import org.hamcrest.Matchers.`is` を使用できます。

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

StatisticsUtilsTest.kt

import com.example.android.architecture.blueprints.todoapp.data.Task
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.junit.Test

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero {

        // Create an active tasks (the false makes this active)
        val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
        // Call your function
        val result = getActiveAndCompletedStats(tasks)

        // Check the result
        assertThat(result.activeTasksPercent, `is`(100f))
        assertThat(result.completedTasksPercent, `is`(0f))

    }
}
  1. 更新したテストを実行して、引き続き動作することを確認してください。

この Codelab では、Hacrest について詳しく解説しているわけではありません。詳しくは、公式チュートリアルをご確認ください。

これは演習を行うためのオプション タスクです。

このタスクでは、JUnit と Hamcrest を使用してさらにテストを作成します。また、テスト駆動開発のプラクティスから派生した戦略を使用してテストを作成します。テスト駆動型開発(TDD)はプログラミングの思想の集まりであり、最初に機能コードを記述するのではなく、テストを最初に記述します。次に、テストに合格することを目標に、機能コードを記述します。

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

通常のタスクリストがある場合に使用するテストを作成します。

  1. 完了したタスクが 1 つあり、アクティブなタスクがない場合、activeTasks の割合は 0f になり、完了したタスクの割合は 100f になります。
  2. 完了したタスクが 2 つ、有効なタスクが 3 つある場合、完了した割合は 40f、有効の割合は 60f になります。

ステップ 2: バグのテストを作成する

記述された getActiveAndCompletedStats のコードにはバグがあります。リストが空または null の場合、適切に処理されないことに注意してください。どちらの場合も、両方の割合はゼロになります。

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

   val totalTasks = tasks!!.size
   val numberOfActiveTasks = tasks.count { it.isActive }
   val activePercent = 100 * numberOfActiveTasks / totalTasks
   val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks

   return StatsResult(
       activeTasksPercent = activePercent.toFloat(),
       completedTasksPercent = completePercent.toFloat()
   )
  
}

コードを修正してテストを作成するには、テスト駆動型開発を使用します。テスト駆動開発は以下の手順を行います。

  1. 「した場合」「タイミング」「タイミング」「次の構文」で、規則に従った名前でテストを記述します。
  2. テストが失敗することを確認します。
  3. テストに合格するには、最小限のコードを記述する必要があります。
  4. すべてのテストでこの手順を繰り返します。

バグの修正から始めるのではなく、最初にテストを作成します。今後、このようなバグの再発を防止するためのテストが実施されていることを確認できます。

  1. 空のリスト(emptyList())がある場合は、両方の割合を 0f にします。
  2. タスクの読み込み中にエラーが発生しました。リストは null になり、両方の割合は 0f になります。
  3. テストを実行して、失敗することを確認します。

ステップ 3: バグの修正

テストが完了したら、バグを修正します。

  1. tasksnull または空の場合に 0f を返すように getActiveAndCompletedStats のバグを修正します。
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

    return if (tasks == null || tasks.isEmpty()) {
        StatsResult(0f, 0f)
    } else {
        val totalTasks = tasks.size
        val numberOfActiveTasks = tasks.count { it.isActive }
        StatsResult(
            activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
            completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
        )
    }
}
  1. テストを再度実行し、すべてのテストに合格したことを確認します。

TDD に沿ってテストを先に記述することで、次のことを確認できました。

  • 新しい機能には常に関連するテストがあります。そのため、テストはコードの動作に関するドキュメントとして機能します。
  • テストでは、正しい結果を確認し、すでに確認したバグから保護します。

解決策: 追加のテストを作成する

すべてのテストと対応する機能コードは次のとおりです。

StatisticsUtilsTest.kt

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero {
        val tasks = listOf(
            Task("title", "desc", isCompleted = false)
        )
        // When the list of tasks is computed with an active task
        val result = getActiveAndCompletedStats(tasks)

        // Then the percentages are 100 and 0
        assertThat(result.activeTasksPercent, `is`(100f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }

    @Test
    fun getActiveAndCompletedStats_noActive_returnsZeroHundred() {
        val tasks = listOf(
            Task("title", "desc", isCompleted = true)
        )
        // When the list of tasks is computed with a completed task
        val result = getActiveAndCompletedStats(tasks)

        // Then the percentages are 0 and 100
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(100f))
    }

    @Test
    fun getActiveAndCompletedStats_both_returnsFortySixty() {
        // Given 3 completed tasks and 2 active tasks
        val tasks = listOf(
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = false),
            Task("title", "desc", isCompleted = false)
        )
        // When the list of tasks is computed
        val result = getActiveAndCompletedStats(tasks)

        // Then the result is 40-60
        assertThat(result.activeTasksPercent, `is`(40f))
        assertThat(result.completedTasksPercent, `is`(60f))
    }

    @Test
    fun getActiveAndCompletedStats_error_returnsZeros() {
        // When there's an error loading stats
        val result = getActiveAndCompletedStats(null)

        // Both active and completed tasks are 0
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }

    @Test
    fun getActiveAndCompletedStats_empty_returnsZeros() {
        // When there are no tasks
        val result = getActiveAndCompletedStats(emptyList())

        // Both active and completed tasks are 0
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }
}

StatisticsUtils.kt

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

    return if (tasks == null || tasks.isEmpty()) {
        StatsResult(0f, 0f)
    } else {
        val totalTasks = tasks.size
        val numberOfActiveTasks = tasks.count { it.isActive }
        StatsResult(
            activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
            completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
        )
    }
}

テストの作成と実行の基礎を習得しました。次は、基本的な ViewModel テストと LiveData テストの作成方法について学びます。

Codelab の残りの部分では、ほとんどのアプリで共通する 2 つの Android クラス(ViewModel および LiveData)のテストを作成する方法を学習します。

まず、TasksViewModel のテストを作成します。


ビューモデル内にすべてのロジックがあり、リポジトリ コードに依存しないテストに焦点を当てます。リポジトリ コードには非同期コード、データベース、ネットワーク呼び出しが含まれているため、テストが複雑になります。当面はそれを避け、リポジトリ内の項目を直接テストしない ViewModel 機能のテストを作成することに専念します。



このテストでは、addNewTask メソッドを呼び出すと、新しいタスク ウィンドウを開く Event が発生することを確認します。こちらがテストするアプリコードです。

TasksViewModel.kt

fun addNewTask() {
   _newTaskEvent.value = Event(Unit)
}

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

StatisticsUtilTest と同じ手順で、TasksViewModelTest のテストファイルを作成します。

  1. tasks パッケージ(TasksViewModel.)で、テストするクラスを開きます。
  2. コードで、クラス名 TasksViewModel -> [Generate] -> [Test] を右クリックします。

  1. [Create Test] 画面で、[OK] をクリックして同意します(デフォルトの設定を変更する必要はありません)。
  2. [Choose Destination Directory] ダイアログで [test] ディレクトリを選択します。

ステップ 2: ViewModel テストを作成する

このステップでは、ビューモデルのテストを追加して、addNewTask メソッドを呼び出すと、新しいタスク ウィンドウを開くための Event が呼び出されます。

  1. addNewTask_setsNewTaskEvent という新しいテストを作成します。

TasksViewModelTest.kt

class TasksViewModelTest {

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh TasksViewModel


        // When adding a new task


        // Then the new task event is triggered

    }
    
}

アプリケーションのコンテキストについて

テストする TasksViewModel のインスタンスを作成する場合、そのコンストラクタにはアプリケーション コンテキストが必要です。ただし、このテストでは、アクティビティと UI とフラグメントを含む完全なアプリケーションを作成するわけではないため、アプリケーションのコンテキストを取得するには、どうすればよいでしょうか。

TasksViewModelTest.kt

// Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(???)

AndroidX Test Library には、テスト用のアクティビティやアクティビティなどのコンポーネントのバージョンを提供するクラスやメソッドが含まれています。シミュレートされた Android フレームワーク クラス(アプリ コンテキストなど)を必要とするローカルテストがある場合は、次の手順で AndroidX Test を適切に設定します。

  1. AndroidX Test のコア依存関係と外部依存関係を追加する
  2. Robolectric Testing ライブラリの依存関係を追加する
  3. クラスに AndroidJunit4 テストランナーのアノテーションを追加する
  4. AndroidX Test コードを記述する

これらの手順を完了して、それがどのように機能しているかを理解します。

ステップ 3: Gradle 依存関係を追加する

  1. これらの依存関係をアプリ モジュールの build.gradle ファイルにコピーして、AndroidX Test のコア依存関係と外部依存関係、Robolectric テスト依存関係を追加します。

app/build.gradle

    // AndroidX Test - JVM testing
testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"

    testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"

 testImplementation "org.robolectric:robolectric:$robolectricVersion"

ステップ 4. JUnit テストランナーを追加する

  1. テストクラスの上に @RunWith(AndroidJUnit4::class) を追加します。

TasksViewModelTest.kt

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
    // Test code
}

ステップ 5. AndroidX Test を使用する

この時点で、AndroidX Test ライブラリを使用できます。これには、アプリ コンテキストを取得するメソッド ApplicationProvider.getApplicationContext が含まれます。

  1. AndroidX テスト ライブラリの ApplicationProvider.getApplicationContext() を使用して TasksViewModel を作成します。

TasksViewModelTest.kt

// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
  1. addNewTasktasksViewModel)に発信します。

TasksViewModelTest.kt

tasksViewModel.addNewTask()

この時点で、テストの内容は以下のようになります。

TasksViewModelTest.kt

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        // TODO test LiveData
    }
  1. テストを実行して動作することを確認します。

コンセプト: AndroidX Test の仕組み

AndroidX Test とは

AndroidX Test は、テスト用のライブラリのコレクションです。このクラスには、Application や Activity などのコンポーネントのバージョン(テスト用)を提供するクラスとメソッドが含まれています。例として、デベロッパーが記述したこのコードは、アプリのコンテキストを取得するための AndroidX Test 関数の例です。

ApplicationProvider.getApplicationContext()

AndroidX Test API の利点の 1 つは、ローカルテストとインストルメンテーション テストの両方で動作するように構築されていることです。これには次のような利点があります。

  • ローカルテストまたはインストルメンテーション テストと同じテストを実行できます。
  • ローカルテストとインストルメンテーション テストで異なるテスト API について学習する必要はありません。

たとえば、AndroidX Test ライブラリを使用してコードを作成したため、TasksViewModelTest クラスを test フォルダから androidTest フォルダに移動させても、テストは実行されます。getApplicationContext() の動作は、ローカルテストかインストルメンテーション テストかによって若干異なります。

  • インストルメンテーション テストの場合は、エミュレータの起動時または実際のデバイスへの接続時に、実際のアプリのコンテキストを取得します。
  • ローカルテストの場合、シミュレートされた Android 環境を使用します。

Robolectric とは

AndroidX Test がローカルテストに使用するシミュレートされた Android 環境は、Robolectric によって提供されています。Robolectric: テスト用に Android 環境をシミュレートするライブラリで、エミュレータの起動やデバイス上での実行よりも高速に実行されます。Robolectric 依存関係を使用しない場合は、次のエラーが発生します。

@RunWith(AndroidJUnit4::class)の取り組み

テストランナーは、テストを実行する JUnit コンポーネントです。テストランナーがないと、テストは実行されません。JUnit により提供されるデフォルトのテストランナーで、自動的に取得します。@RunWith は、デフォルトのテストランナーをスワップします。

AndroidJUnit4 テストランナーを使用すると、インストルメンテーション テストかローカルテストかによって、AndroidX Test のテストを個別に行うことができます。

ステップ 6. Robolectric 警告の修正

コードを実行すると、Robolectric が使用されていることに注意してください。

AndroidX Test と AndroidJunit4 のテストランナーのおかげで、Robolectric コードを直接 1 行も書かずに実行できます。

警告が 2 つ表示される場合があります。

  • No such manifest file: ./AndroidManifest.xml
  • "WARN: Android SDK 29 requires Java 9..."

Gradle ファイルを更新すると、No such manifest file: ./AndroidManifest.xml 警告を修正できます。

  1. Gradle ファイルに次の行を追加して、正しい Android マニフェストが使用されるようにします。includeAndroidResources オプションを使用すると、AndroidManifest ファイルを含む単体テストの Android リソースにアクセスできます。

app/build.gradle

    // Always show the result of every unit test when running via command line, even if it passes.
    testOptions.unitTests {
        includeAndroidResources = true

        // ... 
    }

警告 "WARN: Android SDK 29 requires Java 9..." はさらに複雑です。Android Q でテストを実行するには Java 9 が必要です。Java 9 を使用するように Android Studio を設定する代わりに、この Codelab ではターゲットを SDK を 28 に維持します。

まとめ:

  • 純粋なビューモデルのテストは、通常、コードに Android が必要ない test ソースセットで行います。
  • AndroidX Test ライブラリを使用して、Applications や Activity などのコンポーネントのテスト バージョンを入手できます。
  • test ソースセットでシミュレートされた Android コードを実行する必要がある場合は、Robolectric 依存関係と @RunWith(AndroidJUnit4::class) アノテーションを追加できます。

おめでとうございます。AndroidX テスト ライブラリと Robolectric の両方を使用してテストを実行しています。テストは完了していません(まだアサート ステートメントを記述していませんが、単に「// TODO test LiveData」と表示されています)。次に、LiveData を使用してアサート ステートメントを記述する方法を学習します。

このタスクでは、LiveData 値を正しくアサートする方法を学びます。

ここには、addNewTask_setsNewTaskEvent ビューモデル テストなしで中断した場所が表示されます。

TasksViewModelTest.kt

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        // TODO test LiveData
    }
    

LiveData をテストするには、次の 2 つの方法をおすすめします。

  1. InstantTaskExecutorRule を使用する
  2. LiveData の観測を確認する

ステップ 1: InstantTaskExecutorRule を使用する

InstantTaskExecutorRuleJUnit ルールです。@get:Rule アノテーションとともに使用すると、テストの前後に InstantTaskExecutorRule クラスのコードを実行できるようになります(正確なコードを確認するには、キーボード ショートカット Command+B を使用してファイルを表示します)。

このルールにより、アーキテクチャ コンポーネントに関連するすべてのバックグラウンド ジョブが同じスレッドで実行されるため、テスト結果が同期的かつ繰り返し可能な順序で行われるようになります。LiveData のテストを含むテストを作成する場合は、このルールを使用します。

  1. アーキテクチャ コンポーネントのコアテスト ライブラリ(このルールを含む)の Gradle 依存関係を追加します。

app/build.gradle

testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
  1. TasksViewModelTest.kt を開きます。
  2. TasksViewModelTest クラス内に InstantTaskExecutorRule を追加します。

TasksViewModelTest.kt

class TasksViewModelTest {
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()
    
    // Other code...
}

ステップ 2: LiveDataTestUtil.kt クラスを追加する

次のステップは、テスト対象の LiveData が確実に監視されるようにすることです。

LiveData を使用する場合、通常はアクティビティまたはフラグメント(LifecycleOwner)が LiveData を監視します。

viewModel.resultLiveData.observe(fragment, Observer {
    // Observer code here
})

この所見は重要です。LiveData で以下の操作を行うアクティブなオブザーバーが必要です

  • onChanged イベントをトリガーします。
  • 変換をトリガーする。

ビューモデルの LiveData に対して想定される LiveData の動作を取得するには、LifecycleOwner を指定して LiveData を監視する必要があります。

これにより、次の問題が発生します。TasksViewModel テストでは、LiveData を監視するためのアクティビティやフラグメントがありません。この問題を回避するには、observeForever メソッドを使用します。このメソッドは、LifecycleOwner を必要とせずに LiveData を常に監視できます。observeForever を行う場合、忘れずにオブザーバーを削除する必要があります。削除しないと、オブザーバーの漏えいのリスクがあります。

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

@Test
fun addNewTask_setsNewTaskEvent() {

    // Given a fresh ViewModel
    val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())


    // Create observer - no need for it to do anything!
    val observer = Observer<Event<Unit>> {}
    try {

        // Observe the LiveData forever
        tasksViewModel.newTaskEvent.observeForever(observer)

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.value
        assertThat(value?.getContentIfNotHandled(), (not(nullValue())))

    } finally {
        // Whatever happens, don't forget to remove the observer!
        tasksViewModel.newTaskEvent.removeObserver(observer)
    }
}

これは、テストで 1 つの LiveData をモニタリングするための大量のボイラープレート コードです。このボイラープレートは、いくつかの方法で解消できます。オブザーバーを簡単に追加できるように、LiveDataTestUtil という拡張関数を作成します。

  1. test ソースセット内に LiveDataTestUtil.kt という新しい Kotlin ファイルを作成します。


  1. 下記のコードをコピーして貼り付けてください。

LiveDataTestUtil.kt

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException


@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }
    this.observeForever(observer)

    try {
        afterObserve.invoke()

        // Don't wait indefinitely if the LiveData is not set.
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException("LiveData value was never set.")
        }

    } finally {
        this.removeObserver(observer)
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}

これはかなり複雑な方法です。オブザーバーを追加して LiveData 値を取得し、オブザーバーをクリーンアップする getOrAwaitValue という Kotlin 拡張関数を作成します。基本的には、上記の observeForever コードの再利用可能な短いバージョンです。このクラスの詳細については、こちらのブログ投稿をご覧ください。

ステップ 3: getOrAwaitValue を使用してアサーションを書き込む

このステップでは、getOrAwaitValue メソッドを使用して、newTaskEvent がトリガーされたことを確認するアサート ステートメントを記述します。

  1. getOrAwaitValue を使用して newTaskEventLiveData 値を取得します。
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
  1. 値が null ではないことのアサーションを行います。
assertThat(value.getContentIfNotHandled(), (not(nullValue())))

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

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.not
import org.hamcrest.Matchers.nullValue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()


    @Test
    fun addNewTask_setsNewTaskEvent() {
        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.getOrAwaitValue()

        assertThat(value.getContentIfNotHandled(), not(nullValue()))


    }

}
  1. コードを実行し、テスト合格を観察しましょう。

テストの作成方法を確認しました。次は、テストを自分で作成します。このステップでは、習得したスキルを使って、別の TasksViewModel テストの作成を練習します。

ステップ 1: 独自の ViewModel テストを作成する

setFilterAllTasks_tasksAddViewVisible()」と記述します。テストでは、フィルタの種類ですべてのタスクを表示するように設定している場合に、[タスクを追加] ボタンが表示されていることを確認します。

  1. 参照用に addNewTask_setsNewTaskEvent() を使用して、フィルタモードを ALL_TASKS に設定し、tasksAddViewVisible LiveData が true であることのアサーションを行う、setFilterAllTasks_tasksAddViewVisible() というテストを TasksViewModelTest に記述します。


まずは、以下のコードをご利用ください。

TasksViewModelTest

    @Test
    fun setFilterAllTasks_tasksAddViewVisible() {

        // Given a fresh ViewModel

        // When the filter type is ALL_TASKS

        // Then the "Add task" action is visible
        
    }

注:

  • すべてのタスクの TasksFilterType 列挙型は ALL_TASKS. です。
  • タスクを追加するボタンを表示するかどうかは、LiveData tasksAddViewVisible. で制御します
  1. テストを実行します。

ステップ 2: テストとソリューションを比較する

このソリューションと下記のソリューションを比較してください。

TasksViewModelTest

    @Test
    fun setFilterAllTasks_tasksAddViewVisible() {

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue(), `is`(true))
    }

次の点を確認します。

  • 同じ AndroidX ApplicationProvider.getApplicationContext() ステートメントを使用して tasksViewModel を作成します。
  • setFiltering メソッドを呼び出して、ALL_TASKS フィルタタイプの列挙型を渡します。
  • getOrAwaitNextValue メソッドを使用して、tasksAddViewVisible が true であることを確認します。

ステップ 3: @Before ルールを追加する

両方のテストの開始時に、TasksViewModel を定義することに注意してください。

TasksViewModelTest

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

複数のテストでセットアップ コードを繰り返す場合は、@Before アノテーションを使用してセットアップ メソッドを作成し、繰り返したコードを削除できます。これらのテストはすべて TasksViewModel をテストするので、ビューモデルが必要です。このコードを @Before ブロックに移動します。

  1. tasksViewModel| という lateinit インスタンス変数を作成します。
  2. setupViewModel というメソッドを作成します。
  3. @Before アノテーションを付けます。
  4. ビューモデルのインスタンス化コードを setupViewModel に移動します。

TasksViewModelTest

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

    @Before
    fun setupViewModel() {
        tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
    }
  1. コードを実行します。

警告

次のことは行わないでください。

tasksViewModel

次のように定義します。

val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

これにより、すべてのテストに同じインスタンスが使用されます。各テストでは、テスト対象(この場合は ViewModel)の新しいインスタンスを使用する必要があるため、これは避ける必要があります。

TasksViewModelTest の最終コードは以下のようになります。

TasksViewModelTest

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

    // Executes each task synchronously using Architecture Components.
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()

    @Before
    fun setupViewModel() {
        tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
    }


    @Test
    fun addNewTask_setsNewTaskEvent() {

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.awaitNextValue()
        assertThat(
            value?.getContentIfNotHandled(), (not(nullValue()))
        )
    }

    @Test
    fun getTasksAddViewVisible() {

        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.awaitNextValue(), `is`(true))
    }
    
}

開始したコードと最終的なコードの差分を確認するには、こちらをクリックしてください。

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

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


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

ZIP をダウンロード

この Codelab では以下を行いました。

  • Android Studio からテストを実行する方法。
  • ローカルテスト(test)とインストルメンテーション テスト(androidTest)の違い。
  • JUnitHamcrest を使用してローカル単体テストを作成する
  • AndroidX Test Library を使用して ViewModel テストを設定する

Udacity コース:

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

動画:

その他:

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