Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

LiveData Observe可能なデータホルダー Androidのライフサイクル(Activity/Fragment/Service)を意識して動 作 ViewModelやRoomなどその他のArchitecture componentと連携し て使われることが多い

Slide 6

Slide 6 text

LiveDataのテスト ・ ObserveForever ・ InstantTaskExecutorRule

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

ViewModelのテスト ・ ViewModelScope ・ SavedStateHandler

Slide 14

Slide 14 text

典型的な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 !") } } }

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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 }

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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)

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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する

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Navigationのテスト ・ with Fragment ・ with Activity

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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