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でも同じようにテストできるので切り分ける必要はない。
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