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
メールのエイリアス機能を履き違えない
isshinfunada
0
220
言葉の格闘技のススメ~紙とペンと言葉から始める、キャリアの描き方~
progresscicada
2
140
Apache Hive: Toward a Cloud Native Lakehouse
okumin
0
180
yield再入門 #phpcon
o0h
PRO
0
980
仕様駆動開発の消費期限
watany
15
7k
変わらないものが、変わるものを決める — 意図駆動開発 × イベントソーシング × イミュータブル | What Doesn't Change Decides What Can — IDD × Event Sourcing × Immutability
tomohisa
0
1.3k
プロポーザルを書いてもらう
pvcresin
0
280
Cloudflare is Agents
chimame
0
140
為什麼你並不需要ViewModel / No, you don't need a ViewModel
lovee
1
480
FDEが実現するAI駆動経営の現在地
gonta
2
270
「寝てても仕事が進む」Claude Codeで組む第二の脳
tomoyafujita2016
0
310
これって Effect でできたのでは? / TSKaigi Mashup Kansai #2
susisu
0
150
Featured
See All Featured
Done Done
chrislema
186
16k
Code Review Best Practice
trishagee
74
20k
Darren the Foodie - Storyboard
khoart
PRO
3
3.5k
End of SEO as We Know It (SMX Advanced Version)
ipullrank
3
4.3k
<Decoding/> the Language of Devs - We Love SEO 2024
nikkihalliwell
1
280
StorybookのUI Testing Handbookを読んだ
zakiyama
31
6.9k
世界の人気アプリ100個を分析して見えたペイウォール設計の心得
akihiro_kokubo
PRO
73
41k
Principles of Awesome APIs and How to Build Them.
keavy
128
18k
Being A Developer After 40
akosma
91
590k
The agentic SEO stack - context over prompts
schlessera
0
860
Test your architecture with Archunit
thirion
1
2.3k
Improving Core Web Vitals using Speculation Rules API
sergeychernyshev
21
1.6k
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'