How do I test a viewModelScope controlled SharedFlow?

Viewed 557

I have a SharedFlow. When the ViewModel is created, I change the value to Val1. After that, I use the viewModelScope to make some fake delay of 3 seconds and then change the value to Val2.

class MyViewModel : ViewModel() {

    val x = MutableSharedFlow<String>()

    init {
        x.tryEmit("Val1")
        viewModelScope.launch {
            delay(3000)
            x.tryEmit("Val2")
        }
    }
}

Question

  1. How do I test the initial value is Val1?
  2. How do I test if the value has changed to Val2 after delay?
2 Answers

I found the solution:

It's as simple as setting the Main dispatcher to TestCoroutineDispatcher.

@ExperimentalCoroutinesApi
class CoroutineMainExtension : BeforeEachCallback, AfterEachCallback {
    val dispatcher = TestCoroutineDispatcher()
    override fun beforeEach(context: ExtensionContext?) {
        Dispatchers.setMain(dispatcher)
    }

    override fun afterEach(context: ExtensionContext?) {
        Dispatchers.resetMain()
        dispatcher.cleanupTestCoroutines()
    }
}

and use it like this:

@ExtendWith(CoroutineMainExtension::class)

To test it, you need a way to inject your testing context. It is typically done by setting it as Dispatchers.Main.

Then the easy path is to use MutableStateFlow instead of MutableSharedFlow. Here is an example:

class MyViewModel : ViewModel() {

    val x = MutableStateFlow("Val1")

    init {
        viewModelScope.launch {
            delay(3000)
            x.tryEmit("Val2")
        }
    }
}

class MyViewModelTests {
    private val testDispatcher = TestCoroutineDispatcher()

    @Before
    fun setUp() {
        Dispatchers.setMain(testDispatcher)
    }

    @Test
    fun test() = runBlocking {
        // given
        val myViewModel = MyViewModel()

        // then
        assertEquals("Val1", myViewModel.x.value)

        // when
        testDispatcher.advanceTimeBy(3000)

        // then
        assertEquals("Val2", myViewModel.x.value)
    }
}

If you want to test MutableSharedFlow, you should better move your logic from the constructor to some function, like onCreate. Then you should collect and observe how your values change. Here is an example (we could make a better one with some testing library like Turbine):

class MyViewModel : ViewModel() {

    val x = MutableSharedFlow<String>()

     fun onCreate() {
        viewModelScope.launch {
            x.emit("Val1")
            delay(3000)
            x.emit("Val2")
        }
    }
}

class MyViewModelTests {
    private val testDispatcher = TestCoroutineDispatcher()

    @Before
    fun setUp() {
        Dispatchers.setMain(testDispatcher)
    }

    @Test
    fun test() = runBlocking {
        // given
        val myViewModel = MyViewModel()
        var xChangeHistory = mapOf<Long, String>()
        myViewModel.viewModelScope.launch {
            myViewModel.x.collect {
                xChangeHistory += testDispatcher.currentTime to it
            }
        }

        // then
        myViewModel.onCreate()
        testDispatcher.advanceUntilIdle()

        // then
        assertEquals(mapOf(0L to "Val1", 3000L to "Val2"), xChangeHistory)
    }
}
Related