Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
How to test Coroutines in ViewModel
Search
Hiroyuki Kusu
November 11, 2019
Programming
3.1k
2
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
How to test Coroutines in ViewModel
potatotips #66 (
https://potatotips.connpass.com/event/149806/
) の資料
Hiroyuki Kusu
November 11, 2019
More Decks by Hiroyuki Kusu
See All by Hiroyuki Kusu
モノレポのプルリクエストに最近、導入したもの
hkusu
2
600
GitHub composite actions
hkusu
2
450
Android の静的解析における SARIF ファイルの活用
hkusu
0
5.7k
CI_でライブラリのバージョンの変化をレポートする.pdf
hkusu
0
420
Maestro を GitHub Actions で動かす 〜Android編〜
hkusu
1
1.8k
Android の CI(GitHub Actions)の改善で、最近やったこと
hkusu
0
740
Tauri Mobile で生成される Android のコードを見てみる
hkusu
0
1.6k
Custom GitHub Actions を作って Organization 内で共有する
hkusu
1
600
GitHub Actions でユニットテストの結果をレポートする
hkusu
0
3.9k
Other Decks in Programming
See All in Programming
複数の Claude Code が"放置"されてしまう問題をCLI ダッシュボードを自作して解決した話
sumihiro3
1
680
Claude Code全社展開のためにやったことn選~プラグイン302個・コミッター271人を支えるために~
kenchan
5
1.5k
プロポーザルを書いてもらう
pvcresin
0
480
Foundation Models frameworkで画像分析
ryodeveloper
1
610
AWS CDK を「作」ってみた 〜フルスクラッチで見えた CDK の裏側〜 / aws-cdk-from-scratch
gotok365
3
2.8k
仕様書を書く前にハーネスを作る - Agent Native開発は「探索を速く、判定を固く」
gotalab555
4
1.6k
夏だ!祭りだ!祭りとはドメインモデリングでは?
ryugen04
0
250
関東Kaggler会_NVIDIA_Nemotron_コンペ_振り返り
rick_ds
0
550
Flow は今どうなっているか
mizdra
PRO
0
160
2年かけて Deno に DOMMatrix を実装した話 / How I implemented DOMMatrix in Deno over two years
petamoriken
0
210
AWS DevOps AgentのAzure接続機能を検証して見えた活用法/Use Cases Verified for the AWS DevOps Agent's Azure Connectivity Feature
masakiokuda
1
230
torikago - Ruby::Boxで照らすモジュラモノリスの実行境界
se4weed
1
370
Featured
See All Featured
Discover your Explorer Soul
emna__ayadi
2
1.3k
Art, The Web, and Tiny UX
lynnandtonic
304
22k
Side Projects
sachag
455
43k
XXLCSS - How to scale CSS and keep your sanity
sugarenia
249
1.3M
Bioeconomy Workshop: Dr. Julius Ecuru, Opportunities for a Bioeconomy in West Africa
akademiya2063
PRO
1
220
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
32
4.3k
Building a A Zero-Code AI SEO Workflow
portentint
PRO
0
660
The Cult of Friendly URLs
andyhume
79
7k
Why Mistakes Are the Best Teachers: Turning Failure into a Pathway for Growth
auna
0
200
I Don’t Have Time: Getting Over the Fear to Launch Your Podcast
jcasabona
34
2.8k
Typedesign – Prime Four
hannesfritz
42
3.1k
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
9
1.5k
Transcript
How to test Coroutines in ViewModel 2019.11.11 potatotips #66 Hiroyuki
Kusu ( @hkusu_ )
About me
class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val
_itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { _itemListStatus.value = Status.Loading runCatching { withContext(Dispatchers.IO) { itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } } // ... sealed class Status<out T> { object Loading : Status<Nothing>() data class Success<T>(val data: T) : Status<T>() data class Failure(val throwable: Throwable) : Status<Nothing>() } MainViewModel ͜ͷ ViewModel ͷςετίʔυΛॻ͍ͯΈΔ
@ExperimentalCoroutinesApi class TestCoroutinesRule : TestWatcher() { override fun starting(description: Description?)
{ super.starting(description) Dispatchers.setMain(Dispatchers.Unconfined) } override fun finished(description: Description?) { super.finished(description) Dispatchers.resetMain() } } ϝΠϯεϨουΛࠩ͠ସ͑Δ (ରviewModelScope༻)
@ExperimentalCoroutinesApi class MainViewModelTest { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @get:Rule
var testCoroutinesRule = TestCoroutinesRule() private lateinit var mainViewModel: MainViewModel @MockK private lateinit var itemRepository: ItemRepository @Before fun setUp() { MockKAnnotations.init(this, relaxed = true) mainViewModel = MainViewModel(itemRepository) } @Test fun `MainViewModel#loadItemList()`() { // Given val expectItemList = listOf(Item(1, "item1"), Item(2, "item2")) coEvery { itemRepository.getItemList() } answers { expectItemList } val observer = mockk<Observer<Status<List<Item>>>>(relaxUnitFun = true) mainViewModel.itemListStatus.observeForever(observer) // When mainViewModel.loadItemList() // Then coVerify(exactly = 1) { itemRepository.getItemList() } verifySequence { observer.onChanged(Status.Loading) observer.onChanged(Status.Success(expectItemList)) } } } MainViewModelTest ← ඪ४ͷϧʔϧ(ରLiveData༻) ← ઌ΄Ͳ࡞ͬͨϧʔϧ ← RepositoryϞοΫ͢Δ ← LiveDataͷObserverϞοΫ
None
class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val
_itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { _itemListStatus.value = Status.Loading delay(5000) // dleay runCatching { withContext(Dispatchers.IO) { Thread.sleep(2000) // ॏ͍ॲཧ delay(4000) // delay itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } } ͨͩ͠..
None
// ... @Test fun `MainViewModel#loadItemList()`() { // Given val expectItemList
= listOf(Item(1, "item1"), Item(2, "item2")) coEvery { itemRepository.getItemList() } answers { expectItemList } val observer = mockk<Observer<Status<List<Item>>>>(relaxUnitFun = true) mainViewModel.itemListStatus.observeForever(observer) // When mainViewModel.loadItemList() // Then coVerify(exactly = 1) { itemRepository.getItemList() } verifySequence { observer.onChanged(Status.Loading) observer.onChanged(Status.Success(expectItemList)) } } MainViewModelTest ← ϝιουͷॲཧ͕ऴΘΔͷΛͯͯͳ͍
class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val
_itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { _itemListStatus.value = Status.Loading delay(5000) // dleay runCatching { withContext(Dispatchers.IO) { Thread.sleep(2000) // ॏ͍ॲཧ delay(4000) // delay itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } } ← MVVMͰઃܭ͢Δͱવϝιουͷ ΓUnit MainViewModel Ͳ͏ରࡦ͢Δ͔ʁ
ํ๏ᶃ ViewModelͷϝιουͷ ΓͷܕΛ Job ܕʹ͢Δ
class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() { private val
_itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() = viewModelScope.launch { _itemListStatus.value = Status.Loading Thread.sleep(2000) // ॏ͍ॲཧ delay(5000) // dleay runCatching { withContext(Dispatchers.IO) { Thread.sleep(2000) // ॏ͍ॲཧ delay(4000) // delay itemRepository.getItemList() } } .onSuccess { _itemListStatus.value = Status.Success(it) } .onFailure { _itemListStatus.value = Status.Failure(it) } } } MainViewModel ← JobܕΛฦ͢
// ... @Test fun `MainViewModel#loadItemList()`() { // Given val expectItemList
= listOf(Item(1, "item1"), Item(2, "item2")) coEvery { itemRepository.getItemList() } answers { expectItemList } val observer = mockk<Observer<Status<List<Item>>>>(relaxUnitFun = true) mainViewModel.itemListStatus.observeForever(observer) // When runBlocking { mainViewModel.loadItemList().join() } // Then coVerify(exactly = 1) { itemRepository.getItemList() } verifySequence { observer.onChanged(Status.Loading) observer.onChanged(Status.Success(expectItemList)) } } MainViewModelTest JobܕΛjoin()ͯ͠தஅؔԽ
• ςετͷҝʹΓͷܕΛมߋ͢Δ͜ͱʹ߅͕ ͋Δ • 1ͭͷϝιουͰίϧʔνϯΛෳىಈ͍ͨ͠ ߹ඍົʁ
ํ๏ᶄ ςετ࣌ʹ CoroutineScope Λࠩ͠ସ͑Δ
class MainViewModel( private val itemRepository: ItemRepository, coroutineScope: CoroutineScope = MainScope()
) : ViewModel(), CoroutineScope by coroutineScope { private val _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { launch { _itemListStatus.value = Status.Loading Thread.sleep(2000) // ॏ͍ॲཧ delay(5000) // dleay runCatching { withContext(Dispatchers.IO) { Thread.sleep(2000) // ॏ͍ॲཧ delay(4000) // delay itemRepository.getItemList() } } // ... override fun onCleared() { cancel() } // ... MainViewModel viewModelScopeΛར༻͍ͯ͠ͳ͍ͷͰࣗલͰί ϧʔνϯΛΩϟϯηϧ͢Δίʔυ ίϯετϥΫλͰCoroutineScopeΛࠩ ͠ม͑ΕΔΑ͏ʹ͓ͯ͘͠ .. লུͨ͠߹ MainScope()
// ... // @get:Rule // var testCoroutinesRule = TestCoroutinesRule() private
lateinit var mainViewModel: MainViewModel @MockK private lateinit var itemRepository: ItemRepository @Before fun setUp() { MockKAnnotations.init(this, relaxed = true) // mainViewModel = MainViewModel(itemRepository) } @Test fun `MainViewModel#loadItemList()`() { // Given val expectItemList = listOf(Item(1, "item1"), Item(2, "item2")) coEvery { itemRepository.getItemList() } answers { expectItemList } val observer = mockk<Observer<Status<List<Item>>>>(relaxUnitFun = true) runBlocking { mainViewModel = MainViewModel(itemRepository, this) mainViewModel.itemListStatus.observeForever(observer) // When mainViewModel.loadItemList() } // Then coVerify(exactly = 1) { itemRepository.getItemList() } verifySequence { observer.onChanged(Status.Loading) observer.onChanged(Status.Success(expectItemList)) } } MainViewModelTest ← runBlockingͷείʔϓΛViewModel ͢
• viewModelScope ͏͜ͱ͕Ͱ͖ͳ͍ • ςετίʔυͰ runBlocking ͷϒϩοΫͰ ViewModel ͷΠϯελϯεΛ࡞͢Δඞཁ͕͋ Γςετίʔυ͕ॻ͖ͮΒ͍
ํ๏ᶅ TestCoroutineDispatcher Λ͏
open class DispatcherProvider { open val main: CoroutineDispatcher = Dispatchers.Main
open val io: CoroutineDispatcher = Dispatchers.IO } // ... class MainViewModel( private val itemRepository: ItemRepository, private val dispatcherProvider: DispatcherProvider = DispatcherProvider() ) : ViewModel() { private val _itemListStatus = MutableLiveData<Status<List<Item>>>() val itemListStatus = _itemListStatus.distinctUntilChanged() fun loadItemList() { viewModelScope.launch { _itemListStatus.value = Status.Loading Thread.sleep(2000) // ॏ͍ॲཧ delay(5000) // dleay runCatching { withContext(dispatcherProvider.io) { Thread.sleep(2000) // ॏ͍ॲཧ delay(4000) // delay itemRepository.getItemList() } } // ... MainViewModel Dispatchers.XX ࠩ͠ସ͑ΕΔΑ͏ʹ
@ExperimentalCoroutinesApi class TestCoroutinesRule : TestWatcher() { val testCoroutineDispatcher = TestCoroutineDispatcher()
override fun starting(description: Description?) { super.starting(description) Dispatchers.setMain(testCoroutineDispatcher) } override fun finished(description: Description?) { super.finished(description) Dispatchers.resetMain() testCoroutineDispatcher.cleanupTestCoroutines() } } @ExperimentalCoroutinesApi class TestDispatcherProvider(testCoroutineDispatcher: TestCoroutineDispatcher) : DispatcherProvider() { override val main: CoroutineDispatcher = testCoroutineDispatcher override val io: CoroutineDispatcher = testCoroutineDispatcher } TestUtils.kt
// ... @Before fun setUp() { MockKAnnotations.init(this, relaxed = true)
mainViewModel = MainViewModel(itemRepository, TestDispatcherProvider(testCoroutinesRule.testCoroutineDispatcher)) } @Test fun `MainViewModel#loadItemList()`() = testCoroutinesRule.testCoroutineDispatcher.runBlockingTest { // Given val expectItemList = listOf(Item(1, "item1"), Item(2, "item2")) coEvery { itemRepository.getItemList() } answers { expectItemList } val observer = mockk<Observer<Status<List<Item>>>>(relaxUnitFun = true) mainViewModel.itemListStatus.observeForever(observer) // When mainViewModel.loadItemList() advanceUntilIdle() // ςετର͕delayΛར༻͍ͯ͠Δ߹ͷΈهड़ // Then coVerify(exactly = 1) { itemRepository.getItemList() } verifySequence { observer.onChanged(Status.Loading) observer.onChanged(Status.Success(expectItemList)) } } MainViewModelTest ςετ༻ͷεϨουͷΈͰίϧʔνϯ Λಈ͔ͭͭ͠delayΛίϯτϩʔϧ
ͪΐͬͱศརʹ͢Δ
@ExperimentalCoroutinesApi class TestCoroutinesRule : TestWatcher() { private val testCoroutineDispatcher =
TestCoroutineDispatcher() override fun starting(description: Description?) { super.starting(description) Dispatchers.setMain(testCoroutineDispatcher) } override fun finished(description: Description?) { super.finished(description) Dispatchers.resetMain() testCoroutineDispatcher.cleanupTestCoroutines() } fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) { testCoroutineDispatcher.runBlockingTest { block.invoke(this) } } val dispatcherProvider: DispatcherProvider get() = TestDispatcherProvider(testCoroutineDispatcher) } @ExperimentalCoroutinesApi class TestDispatcherProvider(testCoroutineDispatcher: TestCoroutineDispatcher) : DispatcherProvider() { override val main: CoroutineDispatcher = testCoroutineDispatcher override val io: CoroutineDispatcher = testCoroutineDispatcher } fun <T> LiveData<T>.createTestObserver(): Observer<T> { val observer: Observer<T> = mockk(relaxUnitFun = true) this.observeForever(observer) return observer } TestUtils.kt
// ... @Before fun setUp() { MockKAnnotations.init(this, relaxed = true)
mainViewModel = MainViewModel(itemRepository, testCoroutinesRule.dispatcherProvider) } @Test fun `MainViewModel#loadItemList()`() = testCoroutinesRule.runBlockingTest { // Given val expectItemList = listOf(Item(1, "item1"), Item(2, "item2")) coEvery { itemRepository.getItemList() } answers { expectItemList } val observer = mainViewModel.itemListStatus.createTestObserver() // When mainViewModel.loadItemList() advanceUntilIdle() // Then coVerify(exactly = 1) { itemRepository.getItemList() } verifySequence { observer.onChanged(Status.Loading) observer.onChanged(Status.Success(expectItemList)) } } MainViewModelTest
·ͱΊ • ViewModel ্ͷ Coroutines ͷॲཧͷྃΛͬ ͯςετͷ݁ՌΛݕূ͢Δํ๏Λ̏௨Γઆ໌ͨ͠ • جຊతʹ ํ๏ᶅ
Ͱςετ͢ΕΑͦ͞͏
Thank you ! @hkusu_
ࢀߟ: build.gradle implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0-rc01' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-rc01' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-rc01' // ...
testImplementation 'junit:junit:4.12' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.2' testImplementation 'androidx.arch.core:core-testing:2.1.0' testImplementation 'io.mockk:mockk:1.9.3' testImplementation 'com.google.truth:truth:1.0'