Advanced Android in Kotlin (created by google developers training team) 備忘録

はじめに

これは、以下のKotlin/Androidの基礎チュートリアルについて書いた記事の続きです。

moai510.hatenablog.com ※こちらも過去ブログの移行ですが

↑の記事ではGoogle Developers Training teamが作成したKotlin基礎のチュートリアルについての紹介とメモを書いてましたが、今回はそのチュートリアルの続編的な内容のCourceをやったのでその紹介と備忘録として書きます。

Advanced Android in Kotlin

基礎コースと同じく、Google Developers Training teamが作成した発展的内容に関するCourseです。
Advanced Android in Kotlin: Welcome to the course  |  Android Developers

Lessonは全部で6つあり、以下の構成になっています。

  • Lesson 1: Notifications
  • Lesson 2: Advanced Graphics
  • Lesson 3: Animation
  • Lesson 4: Geo
  • Lesson 5: Testing and Dependency Injection
  • Lesson 6: Login

タイトル通りの内容なので特に説明は省きますが、だいたい必要になりそうな機能がカバーされてるのですごく参考になりそうです。
今回は、特にLesson5のテストについて取り上げます。

Lesson5: Testing

このコースでは、簡単なTODOリストのアプリを例にテストの方法を学べます。

基本知識

Android Projectは基本的に以下の3つのSource Set(フォルダー)で構成される。

チュートリアルのアプリの例
com.example.android.architecture.blueprints.todoapp (main)
com.example.android.architecture.blueprints.todoapp (androidTest)
com.example.android.architecture.blueprints.todoapp (test)
  • main: アプリケーションのコードを含む
  • androidTest: instrumented testとして知られるテストを含む
  • test: ローカルテストを含む

ここでいうinstrumented testとlocal testの違いは実行方法にある。

local test (test source set)

ローカル開発マシンのJVMで実行され、エミュレーターまたは物理デバイスを必要としない。
高速で実行されるが、実環境での動作ではないので、忠実度は低い。

local testのコード例

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

instrumented test (androidTest source set)

実際のAndroidバイスまたはエミュレートされたAndroidバイスで実行されるため、実環境での動作を確認できるが、非常に遅い。

instrumented test のコード例

  • Android固有のコードを含む部分のテストを行う
@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)
    }
}

テストの実行方法

Android Studio使ってればボタンぽちぽちでいける。手順は割愛(サイト参照)。

テストのコーディング

Android Studioの機能に、testスタブを生成してくれるという便利機能がある。テスト作成の手順は以下の通り。

  • 目的の関数にカーソルを合わせてメニューを開き、[Generate...]でスタブ作成
  • ダイアログは基本的にそのままでOK
  • 作成先ディレクトリは、android固有のコードを含むかどうかでtest/androidTestを選択する
  • 雛形が作成される
  • テストを記述する

Android Classのテスト

ViewModelのテスト

Android特有のクラスではあるが、ViewModelのコードはAndroidフレームワークやOSに依存するべきではないため、local testとして書く。 Application ContextやActivityなどが必要になる場合は、AndroidX Test Librariesを使うとシミュレートできる。

// Test用のViewModelの生成
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

AndroidX Testライブラリはlocal testでもinstrument testでも同じようにテストできるので切り分ける必要はない。

f:id:tonotech:20200131000323p:plain

LiveDataのテスト

LiveDataのテストでは以下が必要 - InstantTaskExecutorRuleを使用する - LiveDataを観測する

InstantTaskExecutorRuleは、バックグラウンドジョブをシングルスレッドで動くようにするルールで、LiveDataのテストをする際に必要(同期したいので)。 LiveDataを観測するためには、observeForeverメソッドを使用し、LifeCycleOwnerを必要とせずに監視できるようにする。

// コードの例
@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)
    }
}

実際には上だと毎回書くのが面倒なので、便利な拡張関数も示してくれている。

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
}

このようにgetOrAwaitValueと呼ばれるKotlin拡張関数を作成して置いておくことで、以下のように簡潔にLiveDataのAssertionが書ける。

val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
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()))
    }
}

まとめ

ここまで書いてきた基礎知識に加え、実際のテストのコーディングの詳細や便利なライブラリ群、TDDについても紹介している。 綺麗に書く方法まで紹介してくれているので、実際にテストを書く場面になったらまた適宜参照して行きたい。

テストのライブラリ

  • JUnit4:
  • Hamcrest: 可読性の高いAssertionが書けるようにするライブラリ
  • AndroidX Test Library: ActivityやContextなどAndroidクラスが必要なlocal testを実行するために必要なライブラリ
  • AndroidX Architecture Components Core Test Library