Home > front end >  Android ViewModel with StateFlow - testing issue. Test never waits for next value
Android ViewModel with StateFlow - testing issue. Test never waits for next value

Time:01-15

I have search view model like this. searchPoiUseCase doing requests to Room DB. For testing purposes i am using Room.inMemoryDatabaseBuilder.

@HiltViewModel
class SearchVm @Inject constructor(
    private val searchPoiUseCase: SearchPoiUseCase
) : ViewModel() {

    private val queryState = MutableStateFlow("")
    
    @OptIn(FlowPreview::class)
    val searchScreenState = queryState
        .filter { it.isNotEmpty() }
        .debounce(500)
        .distinctUntilChanged()
        .map { query -> searchPoiUseCase(SearchPoiUseCase.Params(query)) }
        .map { result ->
            if (result.isEmpty()) SearchScreenUiState.NothingFound
            else SearchScreenUiState.SearchResult(result.map { it.toListUiModel() })
        }
        .stateIn(
            viewModelScope,
            SharingStarted.WhileSubscribed(5_000),
            SearchScreenUiState.None
        )
    
    fun onSearch(query: String) {
        queryState.value = query
    }

}

On the device this logic works perfectly fine. But i can't succeed with Unit Testing this logic. Here is my unit test:

@OptIn(ExperimentalCoroutinesApi::class)
@HiltAndroidTest
@Config(application = HiltTestApplication::class)
@RunWith(RobolectricTestRunner::class)
class SearchViewModelTest {

    @get:Rule
    var hiltRule = HiltAndroidRule(this)

    @Inject
    lateinit var searchUseCase: SearchPoiUseCase

    lateinit var SUT: SearchVm

    @Before
    fun setup() {
        hiltRule.inject()
        SUT = SearchVm(searchUseCase)
        Dispatchers.setMain(UnconfinedTestDispatcher())
    }

    @After
    fun teardown() {
        Dispatchers.resetMain()
    }

    @Test
    fun `test search view model`() = runTest {
        val collectJob = launch { SUT.searchScreenState.collect() }
        assertEquals(SearchScreenUiState.None, SUT.searchScreenState.value)

        SUT.onSearch("Query")
        assertEquals(SearchScreenUiState.NothingFound, SUT.searchScreenState.value)

        collectJob.cancel()
    }
}

The second assertion always failed. Am i missing something? Thanks in advance!

UPDATED Thanks to Ibrahim Disouki

His solution working for me with one change

 @Test
    fun `test search view model`() = runTest {
        whenever(searchUseCase(SearchPoiUseCase.Params("Query"))).thenReturn(emptyList()) // here you can create another test case when return valid data

        assertEquals(SearchScreenUiState.None, SUT.searchScreenState.value)

        val job = launch {
            SUT.searchScreenState.collect() //now it should work
        }

        SUT.onSearch("Query")
        advanceTimeBy(500) // This is required in order to bypass debounce(500)
        runCurrent() // Run any pending tasks at the current virtual time, according to the testScheduler.

        assertEquals(SearchScreenUiState.NothingFound, SUT.searchScreenState.value)

        job.cancel()
    }

CodePudding user response:

Please check the following references:

Also, your view model can be run with the regular JUnit test runner as it does not contain any specific Android framework dependencies. Check my working and tested version of your unit test:

import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.*
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnitRunner
import org.mockito.kotlin.whenever

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(MockitoJUnitRunner::class)
class SearchViewModelTest {

    @Mock
    private lateinit var searchUseCase: SearchPoiUseCase

    lateinit var SUT: SearchVm

    @Before
    fun setup() {
        Dispatchers.setMain(UnconfinedTestDispatcher())
        SUT = SearchVm(searchUseCase)
    }

    @After
    fun teardown() {
        Dispatchers.resetMain()
    }

    @Test
    fun `test search view model`() = runTest {
        whenever(searchUseCase(SearchPoiUseCase.Params("Query"))).thenReturn(emptyList()) // here you can create another test case when return valid data

        assertEquals(SearchScreenUiState.None, SUT.searchScreenState.value)

        val job = launch {
            SUT.searchScreenState.collect() //now it should work
        }

        SUT.onSearch("Query")

        runCurrent() // Run any pending tasks at the current virtual time, according to the testScheduler.

        assertEquals(SearchScreenUiState.NothingFound, SUT.searchScreenState.value)

        job.cancel()
    }
}

Another important thing from mocking the SearchPoiUseCase is to manipulating its result to be able to test more cases for example:

  • Return an empty list
  • Return a list of results.
  • etc...
  • Related