3

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()
    }
  • Where is the mocking of the `SearchPoiUseCase`? You should mocking it and make it return mocked value like an empty string in your test case. – Ibrahim Disouki Jan 14 '23 at 14:24
  • The use case is injected by hilt, and inside it is referring to mock data base (through repo instance). Why mocking of use case will make difference? I tried to mock repository that is used in use case, but this didn't help at all. – Nikita Grishko Jan 14 '23 at 15:21
  • When we unit test some class we should mock its dependencies. In your case the `SearchVm` is depending on `SearchPoiUseCase` so you have to mock it. And if your use case `SearchPoiUseCase` is depending on a repository then you should mock this repository when you are testing it. – Ibrahim Disouki Jan 14 '23 at 15:45

1 Answers1

1

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...
Ibrahim Disouki
  • 2,642
  • 4
  • 21
  • 52
  • 1
    Thank you very much, your solution is working for me with one change. I also needed `advanceTimeBy(600)`, in order to pass ` .debounce(500)` Otherwise it is still failing – Nikita Grishko Jan 14 '23 at 19:03