Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
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
470
Android の静的解析における SARIF ファイルの活用
hkusu
0
5.7k
CI_でライブラリのバージョンの変化をレポートする.pdf
hkusu
0
430
Maestro を GitHub Actions で動かす 〜Android編〜
hkusu
1
1.8k
Android の CI(GitHub Actions)の改善で、最近やったこと
hkusu
0
750
Tauri Mobile で生成される Android のコードを見てみる
hkusu
0
1.6k
Custom GitHub Actions を作って Organization 内で共有する
hkusu
1
610
GitHub Actions でユニットテストの結果をレポートする
hkusu
0
4k
Other Decks in Programming
See All in Programming
re:Inventに行く前に知っておきたい現地参加のノウハウ
nokomoro3
0
170
Heart of Swift Concurrency
koher
0
1k
AGENTS.md Is Not Enough:Build Skills, Don't Download Them
lx_t
0
130
TiDB Cloudのカスタムコントローラーによるオートスケール対応
takaidohigasi
0
140
Starting & Sustaining Code-Based E2E Testing for Non-Coding QA Teams( #jasstniigata )
teyamagu
PRO
1
590
速習iPhone Duo対応
yuukiw00w
2
840
市販E-Readerを乗っ取れ 〜Embedded Swiftで電子ペーパーガジェットを制御する〜
trickart
0
220
新卒PdEのリアル
ryu1013
1
520
FreeBSDでZabbixを動かす
kenkino
0
330
KiroのSpecで「五目並べ」を作ってみる
satoshi256kbyte
1
260
wkhtmltopdfの次どうするか問題2026
willnet
2
1.8k
JPUG勉強会 OSSデータベースの内部構造を理解しよう(第2回)
oga5
0
270
Featured
See All Featured
Un-Boring Meetings
codingconduct
0
430
Jess Joyce - The Pitfalls of Following Frameworks
techseoconnect
PRO
1
420
Typedesign – Prime Four
hannesfritz
42
3.2k
What's in a price? How to price your products and services
michaelherold
247
13k
GraphQLの誤解/rethinking-graphql
sonatard
75
12k
Accessibility Awareness
sabderemane
1
220
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
33
5k
Sam Torres - BigQuery for SEOs
techseoconnect
PRO
0
550
Intergalactic Javascript Robots from Outer Space
tanoku
273
27k
Technical Leadership for Architectural Decision Making
baasie
3
580
Money Talks: Using Revenue to Get Sh*t Done
nikkihalliwell
0
510
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
50
10k
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'