ViewModel Unit Testing (JUnit5, CoroutineDispatcher, Turbine, Mockk) Not working as expected

177 views Asked by At

I'm trying to understand how Turbine works with StateFlow.

HelloWorldViewModel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class HelloWorldViewModel(
    private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
    private val doOperationUseCase: DoOperationUseCase
) : ViewModel() {

    private val _state: MutableStateFlow<HelloWorldState> = MutableStateFlow(HelloWorldState())
    val state: StateFlow<HelloWorldState> = _state.asStateFlow()

    fun doOperation() {
        viewModelScope.launch(dispatcher) {
            _state.emit(state.value.copy(loading = true))
            doOperationUseCase()
            _state.emit(state.value.copy(loading = false))
        }
    }

}

HelloWorldState

data class HelloWorldState(val loading: Boolean = false)

DoOperationUseCase

class DoOperationUseCase {
    suspend operator fun invoke(): Result<List<String>> {
        delay(500)
        return Result.success(listOf("1"))
    }
}

HelloWorldViewModelTest

import app.cash.turbine.test
import io.mockk.coEvery
import io.mockk.impl.annotations.MockK
import io.mockk.junit5.MockKExtension
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@OptIn(ExperimentalCoroutinesApi::class)
@ExtendWith(MockKExtension::class)
class HelloWorldViewModelTest {

    private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()

    @MockK
    lateinit var doOperationUseCase: DoOperationUseCase

    private lateinit var classUnderTest: HelloWorldViewModel

    @BeforeEach
    fun setUp() {
        Dispatchers.setMain(testDispatcher)
        classUnderTest = HelloWorldViewModel(testDispatcher, doOperationUseCase)
    }

    @AfterEach
    fun tearDown() {
        Dispatchers.resetMain()
    }

    @Test
    fun doOperation() = runTest {

        coEvery { doOperationUseCase() } returns Result.success(listOf(""))
        assertEquals(HelloWorldState(), classUnderTest.state.value) // expected for initial state
        classUnderTest.state.test {
            classUnderTest.doOperation()
            assertEquals(classUnderTest.state.value.copy(loading = true), awaitItem()) // expected before calling doOperationUseCase use case
            assertEquals(classUnderTest.state.value.copy(loading = false), awaitItem()) // expected after calling doOperationUseCase use case
        }
    }
}

Result:

No value produced in 3s
app.cash.turbine.TurbineAssertionError: No value produced in 3s

Only test success when:

@Test
fun doOperation() = runTest {

    coEvery { doOperationUseCase() } returns Result.success(listOf(""))

    classUnderTest.state.test {
        classUnderTest.doOperation()
        assertEquals(classUnderTest.state.value.copy(loading = false), awaitItem())
    }
}

Dependencies:

testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.2")
testImplementation("org.junit.platform:junit-platform-console:1.8.2")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.8.2")
testImplementation("app.cash.turbine:turbine:1.0.0")
testImplementation("io.mockk:mockk:1.13.8")
testImplementation("com.google.truth:truth:1.1.4")

Google documentation is not helpful as the repository controls when to emit a new value.

0

There are 0 answers