I am trying to write unit test for my application. I am using Mockk to mock classes.
This is my unit test
@OptIn(ExperimentalCoroutinesApi::class)
class MainViewModalTest {
private lateinit var viewModel: MainViewModal
private val savedStateHandle = mockk<SavedStateHandle>(relaxed = true)
private val randomFactUseCase = mockk<RandomFactUseCase>()
@get:Rule
val mainDispatcher = MainDispatcherRule()
@Before
fun setup() {
viewModel = MainViewModal(randomFactUseCase, savedStateHandle)
}
@Test
fun `When fetchRandomFact succeeds, update UI state with fact`() = runBlockingTest {
// Given
coEvery { randomFactUseCase.invoke() } returns Result.Success(RandomFact(data = listOf("Some interesting fact")), 200)
// When
viewModel.fetchRandomFact()
// Then
val expectedUiState = UiState(loading = false, fact = "Some interesting fact", error = "")
assert(viewModel.uiState.value == expectedUiState)
}
}
class MainDispatcherRule(private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()) :
TestWatcher() {
override fun starting(description: Description) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
super.finished(description)
Dispatchers.resetMain()
}
}
it is failing with this error
Exception in thread "Test worker @coroutine#1" io.mockk.MockKException: no answer found for RandomFactUseCase(#2).invoke(continuation {}) among the configured answers: ()
This is my viewModel:
@HiltViewModel
class MainViewModal @Inject constructor(
private val randomFactUseCase: RandomFactUseCase,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
init {
fetchRandomFact()
}
val uiState: StateFlow<UiState> = savedStateHandle.getStateFlow("fact", UiState(loading = true))
fun fetchRandomFact() {
viewModelScope.launch {
setState(loading = true)
when (val response = randomFactUseCase()) {
is Result.Success -> {
val fact =
if (response.data?.data?.isNotEmpty() == true) response.data.data[0] else ""
setState(
loading = false,
fact = fact,
error = if (fact.isBlank()) "No fact available!" else ""
)
}
is Result.Error -> {
setState(
loading = false,
fact = "",
error = response.exception.message
?: "Something went wrong! Please try again"
)
}
}
}
}
private fun setState(loading: Boolean = true, fact: String = "", error: String = "") {
savedStateHandle["fact"] = UiState(loading = loading, fact = fact, error = error)
}
}
@Parcelize
data class UiState(val loading: Boolean = true, val fact: String = "", val error: String = "") :
Parcelable
Not sure what I am missing here. Although I feel that I am pretty close.
EDIT1: As per @broots suggestion I have updated the viewModel like this:
@HiltViewModel
class MainViewModal @Inject constructor(
private val randomFactUseCase: RandomFactUseCase,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
init {
invokeFunction()
}
fun invokeFunction() {
viewModelScope.launch {
fetchRandomFact()
}
}
val uiState: StateFlow<UiState> = savedStateHandle.getStateFlow("fact", UiState(loading = true))
suspend fun fetchRandomFact() {
setState(loading = true)
when (val response = randomFactUseCase()) {
is Result.Success -> {
val fact =
if (response.data?.data?.isNotEmpty() == true) response.data.data[0] else ""
setState(
loading = false,
fact = fact,
error = if (fact.isBlank()) "No fact available!" else ""
)
}
is Result.Error -> {
setState(
loading = false,
fact = "",
error = response.exception.message ?: "Something went wrong! Please try again"
)
}
}
}
private fun setState(loading: Boolean = true, fact: String = "", error: String = "") {
savedStateHandle["fact"] = UiState(loading = loading, fact = fact, error = error)
}
}
It did not solve the issue. (Adding this for reference)
You are using test dispatcher that performs actions directly when called, and in your viewmodel you have call to usecase inside constructor. This way the mock is called still being inside @Before block
If you want to define mock responses in test body, then you could make viewmodel variable lazy
private val savedStateHandle = mockk<SavedStateHandle>(relaxed = true)
private val randomFactUseCase = mockk<RandomFactUseCase>()
private val viewModel: MainViewModal by lazy {
MainViewModal(randomFactUseCase, savedStateHandle)
}
@Test
fun `When fetchRandomFact succeeds, update UI state with fact`() = runBlockingTest {
// Given
coEvery { randomFactUseCase.invoke() } returns Result.Success(RandomFact(data = listOf("Some interesting fact")), 200)
// When
viewModel // dereferencing lazy property will call constructor, which calls method under test
// Then
val expectedUiState = UiState(loading = false, fact = "Some interesting fact", error = "")
assert(viewModel.uiState.value == expectedUiState)
}