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
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
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
37
モノレポのプルリクエストに最近、導入したもの
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
620
Other Decks in Programming
See All in Programming
そのリトライ、死んだコネクションを使い回していませんか ── GoのHTTPクライアントとHTTP/2を実プロダクト障害から学び直す
myus4a
0
390
ゲームコントローラやキーボードのファームウェアをSwiftで書く
kishikawakatsumi
1
280
Streamlitで実現する自然言語データアプリ開発
ayumu_yamaguchi
1
340
プロダクトコードからライブラリの境界を見つける
elmetal
PRO
0
100
Are APIs Still Relevant in the AI Era?
soyuka
0
420
AI が書く Go コードの品質を劇的に向上させる Linter: “declscope”
mpyw
0
480
動作中のプログラムの中身をリアルタイムに覗く / Realtime Debugger for CSharp with Roslyn
prota
1
1.8k
Agentic Software Factoryに、すごく賢いIF文を。 / super smart IF statement into the Agentic Software Factory.
rkaga
7
1.8k
re:Inventに行く前に知っておきたい現地参加のノウハウ
nokomoro3
0
360
JAWS-UG 東京支部が始める、JAWS-UG支部コラボ / JAWS-UG lunchtime LT Collaboration
y0hgi
0
180
Starting & Sustaining Code-Based E2E Testing for Non-Coding QA Teams( #jasstniigata )
teyamagu
PRO
1
910
Java 27新機能 / Java 27 new features
kishida
2
200
Featured
See All Featured
HTML-Aware ERB: The Path to Reactive Rendering @ RubyCon 2026, Rimini, Italy
marcoroth
5
770
Measuring & Analyzing Core Web Vitals
bluesmoon
9
1k
Learning to Love Humans: Emotional Interface Design
aarron
275
41k
Sam Torres - BigQuery for SEOs
techseoconnect
PRO
0
560
Max Prin - Stacking Signals: How International SEO Comes Together (And Falls Apart)
techseoconnect
PRO
0
480
Exploring anti-patterns in Rails
aemeredith
4
520
技術選定の審美眼(2025年版) / Understanding the Spiral of Technologies 2025 edition
twada
PRO
120
120k
Navigating Team Friction
lara
192
16k
Code Reviewing Like a Champion
maltzj
528
40k
The #1 spot is gone: here's how to win anyway
tamaranovitovic
4
1.2k
Lightning talk: Run Django tests with GitHub Actions
sabderemane
0
290
個人開発の失敗を避けるイケてる考え方 / tips for indie hackers
panda_program
123
22k
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'