I am trying to stub a class method twice to test both happy and error path, but for some reason MockK is unable to clear stubbed methods per test.
For convenience I know I can use returnsMany or andThen with MockK to specify a second return value, but that will not work in my specific case. Here is the code.
This is the ViewModel which is being tested:
@HiltViewModel
class FavoriteViewModel @Inject constructor(
private val favoriteUseCase: FavoriteUseCase,
) : ViewModel() {
private val _favoriteScreenState = MutableStateFlow(FavoriteScreenState())
val favoriteScreenState = _favoriteScreenState.asStateFlow()
init {
getFavorites()
}
private fun getFavorites() {
viewModelScope.launch(
context = Dispatchers.IO
) {
try {
_favoriteScreenState.value = favoriteScreenState.value.copy(
movieListState = FavoriteScreenMovieListState.Success(
movieList = favoriteUseCase
.getFavorites()
.cachedIn(viewModelScope)
)
)
} catch (e: Exception) {
_favoriteScreenState.value = favoriteScreenState.value.copy(
movieListState = FavoriteScreenMovieListState.Error(R.string.movie_list_error)
)
}
}
}
fun scrollingToTop(scrollToTopAction: Boolean) {
_favoriteScreenState.value = favoriteScreenState.value.copy(
scrollToTop = scrollToTopAction
)
}
fun updateNavigationItemIndex(index: Int) {
NavigationBarItemSelection.selectedNavigationItemIndex = index
}
}
And this is the unit test class:
class FavoriteViewModelTest {
@MockK
private lateinit var favoriteUseCase: FavoriteUseCase
@InjectMockKs
private lateinit var favoriteViewModel: FavoriteViewModel
private val testDispatcher = StandardTestDispatcher()
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
MockKAnnotations.init(this)
}
@OptIn(ExperimentalCoroutinesApi::class)
@After
fun tearDown() {
clearMocks(favoriteUseCase)
Dispatchers.resetMain()
}
@Test
fun `Load favorite movies in ViewModel instantiation - Happy path`() = runTest {
coEvery { favoriteUseCase.getFavorites() } returns flowOf(PagingData.from(FakeData.fakeMovieFavoriteData)).map {
it.map { movieFavoriteEntity ->
movieFavoriteEntity.movie.toDomain()
}
}
favoriteViewModel.favoriteScreenState.test {
val favoriteScreenState = awaitItem()
when (val movieListState = favoriteScreenState.movieListState) {
is FavoriteScreenMovieListState.Error,
FavoriteScreenMovieListState.Loading -> {
fail("Not expected behavior")
}
is FavoriteScreenMovieListState.Success -> {
val movieList = movieListState.movieList.first()
assertThat(movieList).isNotInstanceOf(FavoriteScreenMovieListState.Loading::class.java)
assertThat(movieList).isNotEqualTo(PagingData.empty<Movie>())
assertThat(favoriteScreenState).isNotEqualTo(FavoriteScreenState())
}
}
}
}
@Test
fun `Load favorite movies in ViewModel instantiation - Error path`() = runTest {
coEvery { favoriteUseCase.getFavorites() } returns flow {
throw Exception()
}
favoriteViewModel.favoriteScreenState.test {
val favoriteScreenState = awaitItem()
when (val movieListState = favoriteScreenState.movieListState) {
is FavoriteScreenMovieListState.Error -> {
assertThat(movieListState).isInstanceOf(FavoriteScreenMovieListState.Error::class.java)
assertThat(movieListState.message).isNotNull()
}
FavoriteScreenMovieListState.Loading,
is FavoriteScreenMovieListState.Success,
-> {
fail("Not expected behavior")
}
}
}
}
@Test
fun getFavoriteScreenState() = runTest {
coEvery { favoriteUseCase.getFavorites() } returns flowOf(PagingData.from(FakeData.fakeMovieFavoriteData)).map {
it.map { movieFavoriteEntity ->
movieFavoriteEntity.movie.toDomain()
}
}
favoriteViewModel.favoriteScreenState.test {
val favoriteScreenState = awaitItem()
assertThat(favoriteScreenState).isInstanceOf(FavoriteScreenState::class.java)
assertThat(favoriteScreenState).isNotNull()
assertThat(favoriteScreenState).isNotEqualTo(FavoriteScreenState())
}
}
@Test
fun scrollingToTop() = runTest {
val scrollToTop = true
favoriteViewModel.favoriteScreenState.test {
val previousValue = awaitItem().scrollToTop
favoriteViewModel.scrollingToTop(scrollToTop)
val currentValue = awaitItem().scrollToTop
assertThat(previousValue).isNotEqualTo(currentValue)
assertThat(currentValue).isTrue()
}
}
@Test
fun updateNavigationItemIndex() {
val newNavigationIndex = 3
val previousValue = NavigationBarItemSelection.selectedNavigationItemIndex
favoriteViewModel.updateNavigationItemIndex(newNavigationIndex)
val currentValue = NavigationBarItemSelection.selectedNavigationItemIndex
assertThat(previousValue).isNotEqualTo(currentValue)
assertThat(currentValue).isEqualTo(newNavigationIndex)
}
}
As you can see, I am setting the stub per each test instead of inside the setUp. Even with this configuration, one of the happy/error path tests will pass and the other will fail.
Have you found a workaround for this specific case? Or is there any solution I am missing? I have read MockK documentation and the only answer I found was using andThen or returnsMany, but that will not apply to my case.