テストの基本

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

はじめに

最初のアプリの最初の機能を実装したときに、コードを実行して想定どおりに動作することを確認したでしょう。テスト手動テスト)を実施しました。機能を追加、更新するたびに、コードを実行して動作を確認していたことでしょう。ただし、これを毎回手動で行うのは面倒で、ミスが発生しやすく、スケーリングできません。

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

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

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

前提となる知識

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

学習内容

このコースでは、次のトピックについて学習します。

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

次のライブラリとコードのコンセプトについて説明します。

演習内容

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

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

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

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

ZIP をダウンロード

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

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

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

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

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

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

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

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

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

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

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

.addedittask

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

.data

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

.statistics

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

.taskdetail

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

.tasks

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

.util

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

データレイヤ(.data)

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

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

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

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

ナビゲーション

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

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

  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. test フォルダを開き、ExampleUnitTest.kt ファイルを見つけます。
  2. 右クリックして [Run ExampleUnitTest] を選択します。

画面下部の [実行] ウィンドウに、次の出力が表示されます。

  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 つのテスト)。
  • 通常、アサーション ステートメントが含まれます。

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.statistics で、StatisticsUtils.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] > [Test] を選択します。

[テストを作成] ダイアログが開きます。

  1. クラス名:StatisticsUtilsTest に変更します(StatisticsUtilsKtTest ではなく。テストクラス名に KT が含まれない方が少し見やすくなります)。
  2. その他のデフォルトはそのままにします。JUnit 4 が適切なテスト ライブラリです。宛先パッケージが正しい(StatisticsUtils クラスの場所を反映している)ことを確認し、チェックボックスをオンにする必要はありません(チェックボックスをオンにすると追加のコードが生成されますが、テストは最初から作成します)。
  3. [OK] を押します。

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

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

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

ステップ 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 を右クリックして [実行] を選択します)。

合格する必要があります。

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

テストはコードの動作を説明するドキュメントの役割も果たすため、人間が読めるようにすると便利です。次の 2 つのアサーションを比較します。

assertEquals(result.completedTasksPercent, 0f)

// versus

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

2 つ目のアサーションは、人間が書いた文にずっと近いものになっています。これは、Hamcrest というアサーション フレームワークを使用して記述されています。読みやすいアサーションを作成するのに役立つツールとして、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 では、Hamcrest のすべての詳細を説明しません。詳細については、公式チュートリアルをご覧ください。

これは練習用のオプションのタスクです。

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

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

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

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

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

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

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. Given、When、Then の構造を使用して、命名規則に沿った名前でテストを記述します。
  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 クラス(ViewModelLiveData)のテストを作成する方法を学びます。

まず、TasksViewModel のテストを記述します。


ここでは、すべてのロジックがビューモデルにあり、リポジトリ コードに依存しないテストに焦点を当てます。リポジトリ コードには、非同期コード、データベース、ネットワーク呼び出しが含まれており、これらはすべてテストの複雑さを増大させます。ここでは、リポジトリのものを直接テストしない ViewModel 機能のテストを作成することに集中します。



作成するテストでは、addNewTask メソッドを呼び出したときに、新しいタスク ウィンドウを開く Event が起動されることを確認します。テストするアプリコードは次のとおりです。

TasksViewModel.kt

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

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

StatisticsUtilTest で行ったのと同じ手順で、このステップでは TasksViewModelTest のテストファイルを作成します。

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

  1. [テストの作成] 画面で、[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 のインスタンスを作成する場合、そのコンストラクタには Application Context が必要です。ただし、このテストではアクティビティ、UI、フラグメントを含む完全なアプリケーションを作成しないため、アプリケーション コンテキストをどのように取得すればよいでしょうか?

TasksViewModelTest.kt

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

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

  1. AndroidX Test の core と ext の依存関係を追加する
  2. Robolectric テスト ライブラリの依存関係を追加する
  3. クラスに AndroidJunit4 テストランナーのアノテーションを付ける
  4. AndroidX Test コードを作成する

これらの手順を完了し、その後、それらの手順が連携して何を行うかを理解します。

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

  1. これらの依存関係をアプリ モジュールの build.gradle ファイルにコピーして、AndroidX Test Core と ext の依存関係、および 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 ライブラリを使用できます。これには、Application Context を取得する ApplicationProvider.getApplicationContext メソッドが含まれます。

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

TasksViewModelTest.kt

// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
  1. tasksViewModeladdNewTask さんに電話します。

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 は、テスト用のライブラリのコレクションです。テスト用のアプリケーションやアクティビティなどのコンポーネントのバージョンを提供するクラスとメソッドが含まれています。たとえば、このコードは、アプリケーション コンテキストを取得するための AndroidX Test 関数の例です。

ApplicationProvider.getApplicationContext()

AndroidX Test API のメリットの 1 つは、ローカルテストとインストゥルメント化テストの両方で動作するように構築されていることです。この方法のメリットは次のとおりです。

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

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

  • インストルメンテーション テストの場合、エミュレータの起動時または実機への接続時に提供される実際の Application コンテキストを取得します。
  • ローカルテストの場合は、シミュレートされた 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. 正しい Android マニフェストが使用されるように、gradle ファイルに次の行を追加します。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 が必要です。この Codelab では、Java 9 を使用するように Android Studio を構成するのではなく、ターゲット SDK とコンパイル SDK を 28 に設定します。

まとめると、次のとおりです。

  • 純粋なビューモデル テストは、通常 test ソースセットに配置できます。コードで Android が必要になることは通常ないためです。
  • AndroidX テスト ライブラリを使用すると、アプリケーションやアクティビティなどのコンポーネントのテスト バージョンを取得できます。
  • 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 Rule です。@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 の動作を取得するには、LifecycleOwnerLiveData を監視する必要があります。

これは問題です。TasksViewModel テストでは、LiveData を監視するアクティビティやフラグメントがありません。この問題を回避するには、LifecycleOwner を必要とせずに LiveData を常に監視する observeForever メソッドを使用します。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 であることをアサートする TasksViewModelTestsetFilterAllTasks_tasksAddViewVisible() というテストを記述します。


以下のコードを使用して開始します。

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))
    }

以下の操作を行っているかどうかを確認します。

  • tasksViewModel は、同じ AndroidX ApplicationProvider.getApplicationContext() ステートメントを使用して作成します。
  • 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 ライブラリを使用して ViewModel テストを設定する。

Udacity コース:

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

動画:

その他:

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