$30 off During Our Annual Pro Sale. View Details »

Android Architecture Componentとテスト2019年7月版 / Testing Android Architecture Components in 2019-07

tkmnzm
July 25, 2019

Android Architecture Componentとテスト2019年7月版 / Testing Android Architecture Components in 2019-07

tkmnzm

July 25, 2019
Tweet

More Decks by tkmnzm

Other Decks in Programming

Transcript

  1. Architecture Componentとテストのまとめ
    [2019年7月版]
    DeNA SWETグループ Nozomi Takuma

    View Slide

  2. 自己紹介
    Nozomi Takum
    DeNA SWETグループ
    Androidとテストが好き

    View Slide

  3. Architecture Componentとテスト
    日々進化するArchitecture Component
    テスト周りも同じくアップデートされる
    現時点(2019年7月)での各componentのテストに関するトピックを
    整理

    View Slide

  4. 本日登場するComponent
    ・ LiveData
    ・ ViewModel
    ・ Room
    ・ WorkManager
    ・ Navigation

    View Slide

  5. LiveData
    Observe可能なデータホルダー
    Androidのライフサイクル(Activity/Fragment/Service)を意識して動

    ViewModelやRoomなどその他のArchitecture componentと連携し
    て使われることが多い

    View Slide

  6. LiveDataのテスト
    ・ ObserveForever
    ・ InstantTaskExecutorRule

    View Slide

  7. ObserveForever
    LiveDataをobserveするときのメソッドは2種類
    observe(lifecycleOwner, observer)
    observeFroever(observer)
    テストのときはLifecycleOwnerのインスタンスが不要な
    observeFroeverを使うのが楽

    View Slide

  8. ObserveForever
    // Observer
    にどんな値が渡ってくるかを記録するためのspy
    val spyObserver = spyk>()
    spy
    をobserveForever
    viewmodel.strLiveData.observeForever(spyObserver)
    // strLiveData
    をTest
    にする実装
    viewmodel.changeText("Test")
    verify {
    // onChanged
    の引数をVerify
    で検証
    spyObserver.onChanged("test")
    }

    View Slide

  9. InstantTaskExecutorRule
    先程のテストコードをJunitで実行するとエラーになる
    java.lang.RuntimeException: Method getMainLooper in
    android.os.Looper not mocked.
    InstantTaskExecutorRuleを使用する

    View Slide

  10. InstantTaskExecutorRule
    background executorを差し替えて同期的に実行してくれる
    android.arch.core:core‑testing
    class LiveDataTest {
    @get:Rule var instantExecutorRule
    = InstantTaskExecutorRule()
    }

    View Slide

  11. InstantTaskExecutorRule
    とてもわかりやすい解説
    https://medium.com/@star_zero/livedataのunittest‑
    2b295d2818c1

    View Slide

  12. ViewModel
    UIにひもづくデータの保持と管理を行う
    画面のライフサイクルを意識して動作
    ドメイン層・モデル層へのエンドポイントとして使用されることが
    多い

    View Slide

  13. ViewModelのテスト
    ・ ViewModelScope
    ・ SavedStateHandler

    View Slide

  14. 典型的なViewModelのテスト
    class TaskViewModelTest {
    @get:Rule var instantExecutorRule
    = InstantTaskExecutorRule()
    @Test fun test() {
    val taskRepositoty = mock/fake TaskRepository
    val viewModel = TaskViewModel(taskRepositoty)
    val spyObserver = spyk>()
    viewmodel.notifyText.observeForever(spyObserver)
    viewmodel.addTask("Test")
    verify {
    spyObserver.onChanged("add task:Test
    !")
    }
    }
    }

    View Slide

  15. ViewModelScope
    ViewModelがDestroyするときに自動でcoroutineのキャンセルをし
    てくれる
    ViewModelのextension propertyとして定義
    デフォルトはDispatchers.MainがCoroutine Dispatherとして使われ
    ている

    View Slide

  16. ViewModelScope
    class TaskViewModel() : ViewModel() {
    fun addTask(taskName: String) {
    viewModelScope.launch {
    ..
    }
    }
    }

    View Slide

  17. Testing with ViewModelScope
    Dispathers.Mainをテスト用のCoroutineDispatherに差し替える
    org.jetbrains.kotlinx:kotlinx‑coroutines‑test
    val testDispatcher = TestCoroutineDispatcher()
    @Before
    fun setup() {
    Dispatchers.setMain(testDispatcher)
    }
    @After
    fun teardown() {
    Dispatchers.resetMain()
    }

    View Slide

  18. Testing with ViewModelScope
    Ruleとして定義しておくと便利
    ref: https://bit.ly/2K10suX

    View Slide

  19. Testing with ViewModelScope
    ViewModelScope内でdelayがあるときは、delayした分の時間をすすめ
    て処理を再開してあげる
    val testDispatcher = TestCoroutineDispatcher()
    ..
    @Test
    fun testDelay() {
    val viewModel = TaskViewModel()
    // ViewModelScope
    内でdelay(1_000)
    viewModel.delayMathod()
    // TestCoroutineDispatcher#advanceTimeBy
    // advanceUntilIdle()
    というのもある
    testDispatcher.advanceTimeBy(1_000)
    // assertion
    }

    View Slide

  20. SavedStateHandler
    ViewModelからsavedInstanceStateへのアクセスが可能になる
    androidx.lifecycle:lifecycle‑viewmodel‑savedstate

    View Slide

  21. SavedStateHandler
    class MyViewModel(val savedState: SavedStateHandle)
    : ViewModel() {
    val text = savedStateHandle.getLiveData("text")
    fun updateText() {
    savedStateHandle.set("text", "test")
    }
    }

    View Slide

  22. SavedStateHandler
    class SavedStateViewModelTest {
    @get:Rule
    val testRule = InstantTaskExecutorRule()
    @Test
    fun test() {
    //
    空のSavedStateHandle
    を渡す
    val target = SavedStateViewModel(SavedStateHandle())
    target.updateText()
    val spyObserver = spyk>()
    target.text.observeForever(spyObserver)
    verify(spyObserver).onChanged("test")
    }
    }

    View Slide

  23. Room
    SQLiteへのアクセスを抽象化
    アノテーションから実装を生成
    LiveDataでのテーブル監視、Rxやcoroutineのサポート、ビュー機
    能など高機能

    View Slide

  24. Roomのテスト
    ・ Dao Interface
    ・ in‑memory database
    ・ Robolectric support

    View Slide

  25. Dao Interface
    RoomではDaoをinterfaceで定義する
    それによりDaoを使用するクラスはRoomの実装をテストダブルで
    置き換えるのが容易
    モックで置き換えたり、後述のin‑memory databaseと組み合わせ
    たりが可能

    View Slide

  26. Dao Interface
    class TasksRepository (val tasksDao: TasksDao)
    val db = Room.databaseBuilder(context.applicationContext,
    AppDatabase::class.java, "Tasks.db").build()
    val repo = TasksRepository(db.tasksDao())
    モックへの置き換え
    val mock = mockk()
    val repo = TasksRepository(mock)

    View Slide

  27. in‑memory database
    メモリ上にデータをstoreしてくれる
    プロセスが消えたらデータも消える
    Fakeの用途でテストに使うことが可能

    View Slide

  28. in‑memory database
    @RunWith(AndroidJUnit4::class)
    class TaskDaoTest {
    private lateinit var db: TestDatabase
    @Before
    fun setUp() {
    val context = ApplicationProvider
    .getApplicationContext()
    db = Room.inMemoryDatabaseBuilder(
    context, TestDatabase::class.java).build()
    }
    @Test
    fun test() {
    // Room
    のin‑memory database
    のDAO
    実装を注入
    val repo = TasksRepository(db.tasksDao())
    }
    @After
    fun tearDown() {
    db.close()
    }

    View Slide

  29. Robolectric support
    RoomのテストはLocal Testでも動作する
    ただLocalでのテストは推奨ではない
    https://developer.android.com/training/data‑
    storage/room/testing‑db#host‑machine
    InstrumentationTestでもLocal動作するようにテストファイルを配
    置してあげるのがよさそう

    View Slide

  30. WorkManager
    バッググラウンドタスクの実行・管理を行う
    延期可能&必ず実行したいバックグラウンドタスクを実装するとき
    に使用
    バックグラウンド関連APIの差分を意識しなくてよくなった

    View Slide

  31. WorkManagerのテスト
    ・ WorkerBuilder(WorkManager 2.1.0+)
    ・ WorkerFactor
    ・ Robolectric Support

    View Slide

  32. TestListenableWorkerBuilder
    val context = ApplicationProvider.getApplicationContext()
    val worker = TestListenableWorkerBuilder(
    inputData = workDataOf("key" to "value")
    context = context).build()
    val result = worker.startWork().get()
    assertThat(result, equalTo(Result.success()))

    View Slide

  33. TestWorkerBuilder
    Executorが差し替えられるのがTestListenableWorkerBuilderとの違い
    val worker = TestWorkerBuilder(
    context = context,
    executor = executor,
    inputData = workDataOf("key" to "value")).build()
    val result = worker.startWork().get()
    assertThat(result, equalTo(Result.success()))

    View Slide

  34. WorkerFactory
    通信処理だったりDBへのアクセスだったりにテストにしにくい要素
    に依存することがある
    WorkerFactoryを利用することで任意のコンストラクタを持つ
    WorkManagerを作成可能

    View Slide

  35. WorkerFactory
    通常のWorkManager
    val request = OneTimeWorkRequestBuilder().build()
    WorkManager.getInstance(context).enqueue(request)
    class MyWorker(
    context: Context,
    workerParams: WorkerParameters)

    View Slide

  36. WorkerFactory
    class MyWorkerFactory : WorkerFactory() {
    override fun createWorker(
    appContext: context,
    workerClassName: name,
    workerParameters: params)
    : ListenableWorker? {
    if (name == MyWorker::class.java.name) {
    // MyWorker
    の依存クラスを追加
    return MyWorker(context, params, MyDependency())
    }
    return null
    }
    }

    View Slide

  37. WorkerFactory
    Custom Appliction
    class MyApp : Application() {
    override fun onCreate() {
    super.onCreate()
    val config = Configuration.Builder()
    .setWorkerFactory(MyWorkerFactory()).build()
    WorkManager.initialize(this, config)
    }
    }
    AndroidManifest.xmlでデフォルトのInitializerをremoveする
    android:name="androidx.work.impl.WorkManagerInitializer"
    android:authorities="${applicationId}.workmanager‑init"
    tools:node="remove" />

    View Slide

  38. WorkerFactory
    @Before fun setUp() {
    val context = ApplicationProvider.getApplicationContext()
    val config = Configuration.Builder()
    .setWorkerFactory(TestMyWorkerFactory())
    .build()
    WorkManagerTestInitHelper.
    initializeTestWorkManager(context, config)
    }

    View Slide

  39. Robolectric Support
    WorkManagerのテストはLocal Testでも動作する
    Issueでサポートしない旨の回答があったが、無事に対応された模様
    WorkManagerの初期化をしないと実行できなかった
    @Before fun setUp() {
    val context = ApplicationProvider.getApplicationContext()
    // WorkManagerTestInitHelper
    をつかう
    // WorkManager.initialize(context, config)

    // Instrumentation Test
    で実行するとエラーになる
    WorkManagerTestInitHelper.
    initializeTestWorkManager(context, config)
    }

    View Slide

  40. Navigation
    画面の遷移の実装をサポート
    シンプルな遷移から、AppBarやNavigationDrawerとの連携など
    画面の遷移をNavigation graphで記述する(フォーマットはXML)

    View Slide

  41. Navigationのテスト
    ・ with Fragment
    ・ with Activity

    View Slide

  42. with Fragment
    FragmentScenarioを使用し、Fragmentにmockの
    NavigationControllerをセットする
    Fragmentから別の画面に遷移したときに、NavigationControllerに
    正しい引数が渡されているかを見る

    View Slide

  43. with Fragment
    val navController = mockk(relaxed = true)
    val scenario = launchFragmentInContainer()
    scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
    }
    ...
    verify {
    navController.navigate(TaskFragmentDirections
    .actionTaskFragmentToTaskListFragment())
    }

    View Slide

  44. with Activity
    基本的には遷移した後の画面のUIを検証すればいいので、
    Navigationを意識する必要ない
    内部的にNavigationを使ったActivityScenarioのテストは
    Robolectric上でも動作する

    View Slide

  45. 本日紹介したComponent
    ・ LiveData
    ・ ViewModel
    ・ Room
    ・ WorkManager
    ・ Navigation

    View Slide

  46. Summary
    Architecture Componentはテストのサポートが充実している
    アップデートにより、書けなかったテストが書けるようになること
    もある
    Architecture Component自体の機能も日々アップデートしているの
    で、あわせてテストはどうなる?も気にかけたい

    View Slide

  47. ご清聴ありがとうございました

    View Slide