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
0
46
モノレポのプルリクエストに最近、導入したもの
hkusu
2
600
GitHub composite actions
hkusu
2
480
Android の静的解析における SARIF ファイルの活用
hkusu
0
5.8k
CI_でライブラリのバージョンの変化をレポートする.pdf
hkusu
0
440
Maestro を GitHub Actions で動かす 〜Android編〜
hkusu
1
1.8k
Android の CI(GitHub Actions)の改善で、最近やったこと
hkusu
0
760
Tauri Mobile で生成される Android のコードを見てみる
hkusu
0
1.6k
Custom GitHub Actions を作って Organization 内で共有する
hkusu
1
620
Other Decks in Programming
See All in Programming
Event is what happens
moro
2
770
コードで差別化できない時代の ソフトウェアエンジニアリング / Software Engineering in an Era Where Code Doesn’t Differentiate
kishida
2
180
SREの越境 / SRE Collaboration
y0hgi
2
310
3回つくってわかった、 定期実行ジョブ基盤の本質情報
takai
0
460
UPDATE をやめる — EF Core でマスタをバージョン管理する
panda728
PRO
0
1.2k
最近”作らせてる” ローカルAIアプリの話
akit37
0
470
パズルゲームの作り方 / how to make puzzle games
kaityo256
PRO
2
290
JAWS-UG 東京支部が始める、JAWS-UG支部コラボ / JAWS-UG lunchtime LT Collaboration
y0hgi
0
270
AI主導のプログラミング学習アプリを作ってみた
econanringo
0
180
當我們開始不讀 Code - AI Agent 時代,Code Review 與 Clean Code 還剩下什麼?
jame2408
1
200
ハーネス設計入門 〜 基礎知識の整理から実務へのステップアップ 〜
kinopeee
28
32k
SalesForceを内製化!? ~ HR事業を支える顧客管理基盤のインフラを大公開 ~
oku053
0
180
Featured
See All Featured
How To Speak Unicorn (iThemes Webinar)
marktimemedia
1
600
Intergalactic Javascript Robots from Outer Space
tanoku
273
28k
The Curious Case for Waylosing
cassininazir
1
570
Ethics towards AI in product and experience design
skipperchong
2
430
Digital Ethics as a Driver of Design Innovation
axbom
PRO
1
460
Producing Creativity
orderedlist
PRO
348
41k
AI Search: Implications for SEO and How to Move Forward - #ShenzhenSEOConference
aleyda
1
1.4k
Public Speaking Without Barfing On Your Shoes - THAT 2023
reverentgeek
1
600
From Legacy to Launchpad: Building Startup-Ready Communities
dugsong
0
360
Building Better People: How to give real-time feedback that sticks.
wjessup
370
20k
A better future with KSS
kneath
240
18k
How to Get Subject Matter Experts Bought In and Actively Contributing to SEO & PR Initiatives.
livdayseo
0
220
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'