airbnb / mavericks

Mavericks: Android on Autopilot
https://airbnb.io/mavericks/
Apache License 2.0
5.83k stars 500 forks source link

How to go about unit testing onEach events in Viewmodel #644

Open ssiddh opened 2 years ago

ssiddh commented 2 years ago

I am trying to unit test certain flows on the viewmodel which are triggered onEach subscription of the a specific property. https://airbnb.io/mavericks/#/core-concepts?id=subscribing-to-state-changes

I can correctly verify the state transitions in the unit test, but I am not able to verify functions called in response to onEach subscription. I looked through the documentation and could not find something that could help. Is there a way to achieve this?

example:

class SomeViewModel(initialState: SomeState, tracker: Tracker): MavericksViewModel<SomeState>(initialState) {
   init {
       onEach(SomeState::propertyA) {
           tracker.fireAnalyticsEvent() // Want to verify this function gets called on state changes 
       }
   }
}

class SomeViewModelTest {
    @get:Rule
    val mvrxTestRule = MvRxTestRule(testDispatcher = UnconfinedTestDispatcher())

    @Test
    fun sampleTest() {
        // given
        sut = SomeViewModel(SomeState(a, b, c), MockTracker())

        // when
        sut.doSomething()

        // then
        withState(sut) { state ->
             assertTrue(state.propertyA == a1) // This works
        }
        verify(1) {
            // Verify this function was called once
            tracker.fireAnalyticsEvent() // This fails
        }
    }
}

PS: Thank you for this amazing library, it is a delight to use.

PayamGerackoohi commented 11 months ago

Hi, sorry for the late answer, but if anyone is interested, verify(1) is wrong, because you're setting propertyA twice. First when you set the initial state, then when you call doSomething. The rest works fine for me:

import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.test.MavericksTestRule
import com.airbnb.mvrx.withState
import io.mockk.confirmVerified
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test

data class SomeState(val propertyA: String, val propertyB: String, val propertyC: String) : MavericksState

interface Tracker {
    fun fireAnalyticsEvent()
}

class SomeViewModel(initialState: SomeState, tracker: Tracker) : MavericksViewModel<SomeState>(initialState) {
    init {
        onEach(SomeState::propertyA) {
            println("*** propertyA: $it")
            tracker.fireAnalyticsEvent()
        }
    }

    fun doSomething() = setState { copy(propertyA = "a1") }
}

@OptIn(ExperimentalCoroutinesApi::class)
class SomeViewModelTest {
    @get:Rule
    val mavericksTestRule = MavericksTestRule(testDispatcher = UnconfinedTestDispatcher())

    @Test
    fun sampleTest() {
        // given
        val tracker = mockk<Tracker> { justRun { fireAnalyticsEvent() } }
        val sut = SomeViewModel(SomeState("a", "b", "c"), tracker)

        // when
        sut.doSomething()

        // then
        withState(sut) { state -> assertTrue(state.propertyA == "a1") }
        verify(exactly = 2) { tracker.fireAnalyticsEvent() }

        confirmVerified(tracker)
    }
}

This prints:

*** propertyA: a
*** propertyA: a1