Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How to test Coroutines in ViewModel

Hiroyuki Kusu
November 11, 2019

How to test Coroutines in ViewModel

potatotips #66 ( https://potatotips.connpass.com/event/149806/ ) の資料

Hiroyuki Kusu

November 11, 2019
Tweet

More Decks by Hiroyuki Kusu

Other Decks in Programming

Transcript

  1. How to test Coroutines
    in ViewModel
    2019.11.11 potatotips #66
    Hiroyuki Kusu ( @hkusu_ )

    View full-size slide

  2. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() {
    private val _itemListStatus = MutableLiveData>>()
    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 {
    object Loading : Status()
    data class Success(val data: T) : Status()
    data class Failure(val throwable: Throwable) : Status()
    }
    MainViewModel
    ͜ͷ ViewModel ͷςετίʔυΛॻ͍ͯΈΔ

    View full-size slide

  3. @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༻)

    View full-size slide

  4. @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>>>(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΋ϞοΫ

    View full-size slide

  5. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() {
    private val _itemListStatus = MutableLiveData>>()
    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)
    }
    }
    }
    }
    ͨͩ͠..

    View full-size slide

  6. // ...
    @Test
    fun `MainViewModel#loadItemList()`() {
    // Given
    val expectItemList = listOf(Item(1, "item1"), Item(2, "item2"))
    coEvery { itemRepository.getItemList() } answers { expectItemList }
    val observer = mockk>>>(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
    ← ϝιουͷॲཧ͕ऴΘΔͷΛ଴ͯͯͳ͍

    View full-size slide

  7. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() {
    private val _itemListStatus = MutableLiveData>>()
    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
    Ͳ͏ରࡦ͢Δ͔ʁ

    View full-size slide

  8. ํ๏ᶃ
    ViewModelͷϝιουͷ
    ໭Γ஋ͷܕΛ Job ܕʹ͢Δ

    View full-size slide

  9. class MainViewModel(private val itemRepository: ItemRepository) : ViewModel() {
    private val _itemListStatus = MutableLiveData>>()
    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ܕΛฦ͢

    View full-size slide

  10. // ...
    @Test
    fun `MainViewModel#loadItemList()`() {
    // Given
    val expectItemList = listOf(Item(1, "item1"), Item(2, "item2"))
    coEvery { itemRepository.getItemList() } answers { expectItemList }
    val observer = mockk>>>(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()ͯ͠தஅؔ਺Խ

    View full-size slide

  11. • ςετͷҝʹ໭Γ஋ͷܕΛมߋ͢Δ͜ͱʹ఍߅͕
    ͋Δ
    • 1ͭͷϝιουͰίϧʔνϯΛෳ਺ىಈ͍ͨ͠৔
    ߹͸ඍົʁ

    View full-size slide

  12. ํ๏ᶄ
    ςετ࣌ʹ CoroutineScope
    Λࠩ͠ସ͑Δ

    View full-size slide

  13. class MainViewModel(
    private val itemRepository: ItemRepository,
    coroutineScope: CoroutineScope = MainScope()
    ) : ViewModel(), CoroutineScope by coroutineScope {
    private val _itemListStatus = MutableLiveData>>()
    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()

    View full-size slide

  14. // ...
    // @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>>>(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
    ΁౉͢

    View full-size slide

  15. • viewModelScope ͸࢖͏͜ͱ͕Ͱ͖ͳ͍
    • ςετίʔυͰ runBlocking ͷϒϩοΫ಺Ͱ
    ViewModel ͷΠϯελϯεΛ࡞੒͢Δඞཁ͕͋
    Γςετίʔυ͕ॻ͖ͮΒ͍

    View full-size slide

  16. ํ๏ᶅ
    TestCoroutineDispatcher
    Λ࢖͏

    View full-size slide

  17. 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>>()
    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 ͸ࠩ͠ସ͑ΕΔΑ͏ʹ

    View full-size slide

  18. @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

    View full-size slide

  19. // ...
    @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>>>(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Λίϯτϩʔϧ

    View full-size slide

  20. ͪΐͬͱศརʹ͢Δ

    View full-size slide

  21. @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 LiveData.createTestObserver(): Observer {
    val observer: Observer = mockk(relaxUnitFun = true)
    this.observeForever(observer)
    return observer
    }
    TestUtils.kt

    View full-size slide

  22. // ...
    @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

    View full-size slide

  23. ·ͱΊ
    • ViewModel ্ͷ Coroutines ͷॲཧͷ׬ྃΛ଴ͬ
    ͯςετͷ݁ՌΛݕূ͢Δํ๏Λ̏௨Γઆ໌ͨ͠
    • جຊతʹ͸ ํ๏ᶅ Ͱςετ͢Ε͹Αͦ͞͏

    View full-size slide

  24. Thank you !
    @hkusu_

    View full-size slide

  25. ࢀߟ: 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'

    View full-size slide