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...