I'm trying to write a test for my view model that gets the data from datastore but I can't figure it out. this is my data store implementation. I save the user data such as email and token when user is sign up or sign in :
class FlowAuthenticationDataStore(context: Context) : AuthenticationDataStore {
private val dataStore = context.createDataStore(name = "user_auth")
private val userEmailKey = preferencesKey<String>(name = "USER_EMAIL")
private val userTokenKey = preferencesKey<String>(name = "USER_TOKEN")
private val userNameKey = preferencesKey<String>(name = "USER_NAME")
private val userIsLoginKey = preferencesKey<Boolean>(name = "USER_IS_LOGIN")
override suspend fun updateUser(userDataStore: UserDataStoreModel) {
dataStore.edit {
it[userEmailKey] = userDataStore.email
it[userTokenKey] = userDataStore.token
it[userNameKey] = userDataStore.name
it[userIsLoginKey] = userDataStore.isLogin
}
}
override fun observeUser(): Flow<UserDataStoreModel> {
return dataStore.data.catch {
if (it is IOException) {
emit(emptyPreferences())
} else {
throw it
}
}.map {
UserDataStoreModel(
it[userIsLoginKey]!!,
it[userNameKey]!!,
it[userEmailKey]!!,
it[userTokenKey]!!
)
}
}
}
and this is my view model. I observe the user data store and if its success and has a data then I update my live data. If there is not data and user is first time to register then my live data is equal to default value from data class :
class SplashScreenViewModel(
private val flowOnBoardingDataStore: FlowAuthenticationDataStore,
private val contextProvider: CoroutineContextProvider,
) :
ViewModel() {
private val _submitState = MutableLiveData<UserDataStoreModel>()
val submitState: LiveData<UserDataStoreModel> = _submitState
fun checkUserLogin() {
viewModelScope.launch {
kotlin.runCatching {
withContext(contextProvider.io) {
flowOnBoardingDataStore.observeUser().collect {
_submitState.value = it
}
}
}.onFailure {
_submitState.value = UserDataStoreModel()
}
}
}
}
and this is my test class:
#ExperimentalCoroutinesApi
class SplashScreenViewModelTest {
private val dispatcher = TestCoroutineDispatcher()
#get:Rule
val rule = InstantTaskExecutorRule()
#get:Rule
val coroutineTestRule = CoroutineTestRule(dispatcher)
#RelaxedMockK
lateinit var flowOnBoardingDataStore: FlowAuthenticationDataStore
private fun createViewModel()=SplashScreenViewModel(flowOnBoardingDataStore,
CoroutineContextProvider(dispatcher,dispatcher)
)
#Before
fun setup() {
MockKAnnotations.init(this)
}
#After
fun tearDown() {
unmockkAll()
}
#Test
fun `when user is already sign in, then state should return model`()=dispatcher.runBlockingTest {
val viewModel=createViewModel()
val userDataStoreModel= UserDataStoreModel(true,"test","test","test")
flowOnBoardingDataStore.updateUser(userDataStoreModel)
viewModel.checkUserLogin()
assertEquals(userDataStoreModel,viewModel.submitState.value)
}
}
This is the result of my test function:
junit.framework.AssertionFailedError:
Expected :UserDataStoreModel(isLogin=true, name=test, email=test, token=test)
Actual :null
I find the solution and I posted here Incase anybody needs it.
The solution is using coEvery to return a fake data with flowOf from the usecase( you don't need to use flowOf , its based on your return data from your use case, in my case it's return a flow):
#Test
fun `when user is already sign in, then state should return user data`()=dispatcher.runBlockingTest {
val userData=UserDataStoreModel(true, Name,
Email,"","")
coEvery { authenticationDataStore.observeUser() }returns flowOf(userData)
val viewModel=createViewModel()
viewModel.checkUserLogin()
assertEquals(userData,viewModel.submitState.value)
}
This is the full test class:
#ExperimentalCoroutinesApi
class SplashScreenViewModelTest {
private val dispatcher = TestCoroutineDispatcher()
#get:Rule
val rule = InstantTaskExecutorRule()
#get:Rule
val coroutineTestRule = CoroutineTestRule(dispatcher)
#RelaxedMockK
lateinit var authenticationDataStore: AuthenticationDataStore
private fun createViewModel()=SplashScreenViewModel(authenticationDataStore,
CoroutineContextProvider(dispatcher,dispatcher)
)
#Before
fun setup() {
MockKAnnotations.init(this)
}
#After
fun tearDown() {
unmockkAll()
}
#Test
fun `when user is already sign in, then state should return user data`()=dispatcher.runBlockingTest {
val userData=UserDataStoreModel(true, Name,
Email,"","")
coEvery { authenticationDataStore.observeUser() }returns flowOf(userData)
val viewModel=createViewModel()
viewModel.checkUserLogin()
assertEquals(userData,viewModel.submitState.value)
}
}
Related
I want to write a simple test for my viewModel to check if it gets data from repository. The app itself working without problem but in test, i have the following test failed.
It looks like the viewModel init block not running, because it suppose to call getUpcomingMovies() method in init blocks and post value to upcomingMovies live data object. When i test it gets null value.
Looks like i am missing a minor thing, need help to solve this.
Here is the test:
#ExperimentalCoroutinesApi
class MoviesViewModelShould: BaseUnitTest() {
private val repository: MoviesRepository = mock()
private val upcomingMovies = mock<Response<UpcomingResponse>>()
private val upcomingMoviesExpected = Result.success(upcomingMovies)
#Test
fun emitsUpcomingMoviesFromRepository() = runBlocking {
val viewModel = mockSuccessfulCaseUpcomingMovies()
assertEquals(upcomingMoviesExpected, viewModel.upcomingMovies.getValueForTest())
}
private fun mockSuccessfulCaseUpcomingMovies(): MoviesViewModel {
runBlocking {
whenever(repository.getUpcomingMovies(1)).thenReturn(
flow {
emit(upcomingMoviesExpected)
}
)
}
return MoviesViewModel(repository)
}
}
And viewModel:
class MoviesViewModel(
private val repository: MoviesRepository
): ViewModel() {
val upcomingMovies: MutableLiveData<UpcomingResponse> = MutableLiveData()
var upcomingMoviesPage = 0
private var upcomingMoviesResponse: UpcomingResponse? = null
init {
getUpcomingMovies()
}
fun getUpcomingMovies() = viewModelScope.launch {
upcomingMoviesPage++
repository.getUpcomingMovies(upcomingMoviesPage).collect { result ->
if (result.isSuccess) {
result.getOrNull()!!.body()?.let {
if (upcomingMoviesResponse == null) {
upcomingMoviesResponse = it
} else {
val oldMovies = upcomingMoviesResponse?.results
val newMovies = it.results
oldMovies?.addAll(newMovies)
}
upcomingMovies.postValue(upcomingMoviesResponse ?: it)
}
}
}
}
}
And the result is:
expected:<Success(Mock for Response, hashCode: 1625939772)> but was:<null>
Expected :Success(Mock for Response, hashCode: 1625939772)
Actual :null
I'm trying to test my ViewModel using StateFlow, and I'm having trouble writing a simple error scenario test code. I'm using sealed class to manager the UI Events and UI states, and I'm writing the first unit test for my viewmodel to the Register flow.
ViewModel.kt
#HiltViewModel
class RegisterViewModel #Inject constructor(
private val signupWithEmailAndPasswordUseCase: SignupWithEmailAndPasswordUseCase
) : ViewModel() {
private val registerStateFlow = MutableStateFlow<RegisterState>(RegisterState.Initial)
fun getRegisterStateFlow(): StateFlow<RegisterState> = registerStateFlow
fun onEvent(event: RegisterEvent) {
when (event) {
is RegisterEvent.RegisterNewUser -> {
viewModelScope.launch {
doRegister(event.data)
}
}
}
}
private fun doRegister(user: User) {
//TODO validate all fields
viewModelScope.launch(Dispatchers.IO) {
signupWithEmailAndPasswordUseCase(user).collectLatest {
when (it) {
is NetworkResult.Progress -> registerStateFlow.value = RegisterState.Loading
is NetworkResult.Failure -> registerStateFlow.value = RegisterState.NetworkError
is NetworkResult.Success -> registerStateFlow.value =
RegisterState.Success(it.data)
}
}
}
}
}
My test class
#OptIn(ExperimentalCoroutinesApi::class)
class RegisterViewModelTest{
private val useCase: SignupWithEmailAndPasswordUseCase = mockk(relaxed = true)
private val user = mockk<User>()
private val testDispatcher = StandardTestDispatcher()
#get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
#Before
fun setup(){
Dispatchers.setMain(testDispatcher)
clearAllMocks()
}
#After
fun tearDown(){
Dispatchers.resetMain()
}
#Test
fun `Error state should be shown while register is being loaded`() = runTest{
val viewmodel = RegisterViewModel(useCase)
coEvery { useCase.invoke(user) } returns flow {
emit(NetworkResult.Failure(IOException()))
}
viewmodel.onEvent(RegisterEvent.RegisterNewUser(user))
viewmodel.getRegisterStateFlow().collectIndexed { index, value ->
if (index == 0) assertEquals(RegisterState.Initial, value)
if (index == 1) assertEquals(RegisterState.NetworkError, value)
}
}
}
After a long time runing I receive this error -> After waiting for 60000 ms, the test coroutine is not completing
And I don't know how to solve these problem
I already create some unit test for my view model. but when I println() the result it always return State Loading.. I have tried to read some article and cek in other source code but I'm still not found the answer.
Here is my code from ViewModel :
class PredefineViewModel() : ViewModel() {
private var predefineRepository: PredefineRepository? = PredefineRepository()
private val _predefined = MutableLiveData<String>()
val predefined: LiveData<Resource<Payload<Predefine>>> =
Transformations.switchMap(_predefined) {
predefineRepository?.predefine()
}
fun predefined() {
_predefined.value = "predefined".random().toString()
}
}
Here is my Repository
class PredefineRepository() {
private val api: PredefineApi? = PredefineApi.init()
fun predefine(): BaseMutableLiveData<Predefine> {
val predefine: BaseMutableLiveData<Predefine> = BaseMutableLiveData()
api?.let { api ->
predefine.isLoading()
api.predefined().observe()?.subscribe({ response ->
response?.let { resource ->
predefine.isSuccess(resource)
}
}, { error ->
predefine.isError(error)
})
}
return predefine
}
}
Here is my Resources State :
data class Resource<T>(var status: Status? = null, var meta: Meta? = null, var payload: T? =null) {
companion object {
fun <T> success(data: T?, meta: Meta): Resource<T> {
return Resource(Status.SUCCESS, meta, data)
}
fun <T> error(data: T?, meta: Meta): Resource<T> {
return Resource(Status.ERROR, meta, data)
}
fun <T> loading(data: T?, meta: Meta): Resource<T> {
return Resource(Status.LOADING, null, null)
}
}
}
UPDATE TEST CLASS
And, This is sample I try to print and check value from my live data view model :
class PredefineViewModelTest {
#get:Rule
val taskExecutorRule = InstantTaskExecutorRule()
private lateinit var viewModel: PredefineViewModel
private lateinit var repository: PredefineRepository
private lateinit var api: Api
#Before
fun setUp() {
api = Networks().bridge().create(Api::class.java)
repository = PredefineRepository()
viewModel = PredefineViewModel()
}
#Test
fun test_predefined(){
val data = BaseMutableLiveData<Predefine>()
val result = api.predefined()
result.test().await().assertComplete()
result.subscribe {
data.isSuccess(it)
}
`when`(repository.predefine()).thenReturn(data)
viewModel.predefined()
viewModel.predefined.observeForever {
println("value: $it")
println("data: ${data.value}")
}
}
}
UPDATE LOG Results
Why the result from my predefined always:
value: Resource(status=LOADING, meta=null, payload=null, errorData=[])
data: Resource(status=SUCCESS, meta=Meta(code=200, message=success, error=null), payload= Data(code=200, message=success, errorDara =[])
Thank You..
You would require to mock your API response. The unit test won't run your API actually you have to mock that. Please have a look at the attached snippet, It will give you a basic idea of how you can achieve that.
ViewModel:
class MainViewModel(val repository: Repository) : ViewModel() {
fun fetchData(): LiveData<Boolean> {
return Transformations.map(repository.getData()) {
if (it.status == 200) {
true
} else {
false
}
}
}
}
Repo:
open class Repository {
open fun getData() : LiveData<MainModel> {
return MutableLiveData(MainModel(10, 200))
}
}
Test Class:
#RunWith(MockitoJUnitRunner::class)
class MainViewModelTest {
lateinit var mainModel: MainViewModel
#Rule
#JvmField
var rule: TestRule = InstantTaskExecutorRule()
#Mock
lateinit var repo: Repository
init {
MockitoAnnotations.initMocks(this)
}
#Before
fun setup() {
mainModel = MainViewModel(repo)
}
#Test
fun fetchData_success() {
val mainModelData = MainModel(10, 200)
`when`(repo.getData()).thenReturn(MutableLiveData(mainModelData))
mainModel.fetchData().observeForever {
Assert.assertTrue(it)
}
}
#Test
fun fetchData_failure() {
val mainModelData = MainModel(10, 404)
`when`(repo.getData()).thenReturn(MutableLiveData(mainModelData))
mainModel.fetchData().observeForever {
Assert.assertFalse(it)
}
}
}
I couldn't see your API mock. Your initial status is loading inside LiveData.
{ response ->
response?.let { resource ->
predefine.isSuccess(resource)
}
block is not executing during the test.
I'm new on unit testing. I'm trying to do unit testing on my view model class but my test fail with error:
Wanted but not invoked:
toggleMovieFavorite.invoke(
Movie(id=1, title=Title, overview=Overview, releaseDate=01/01/2025, posterPath=, backdropPath=, originalLanguage=ES, originalTitle=Title, popularity=5.0, voteAverage=7.0, favorite=false)
);
-> at xyz.jonthn.usescases.ToggleMovieFavorite.invoke(ToggleMovieFavorite.kt:7)
Actually, there were zero interactions with this mock.
This is my test file
#RunWith(MockitoJUnitRunner::class)
class DetailViewModelTest {
#get:Rule
val rule = InstantTaskExecutorRule()
#Mock
lateinit var findMovieById: FindMovieById
#Mock
lateinit var toggleMovieFavorite: ToggleMovieFavorite
#Mock
lateinit var observer: Observer<Movie>
private lateinit var vm: DetailViewModel
#ExperimentalCoroutinesApi
#Before
fun setUp() {
Dispatchers.setMain(Dispatchers.Unconfined)
vm = DetailViewModel(1, findMovieById, toggleMovieFavorite, Dispatchers.Unconfined)
}
#ExperimentalCoroutinesApi
#After
fun tearDown() {
Dispatchers.resetMain()
}
#Test
fun `when favorite clicked, the toggleMovieFavorite use case is invoked`() {
runBlocking {
val movie = mockedMovie.copy(id = 1)
whenever(findMovieById.invoke(1)).thenReturn(movie)
whenever(toggleMovieFavorite.invoke(movie)).thenReturn(movie.copy(favorite = !movie.favorite))
vm.movie.observeForever(observer)
vm.onFavoriteClicked()
verify(toggleMovieFavorite).invoke(movie)
}
}
val mockedMovie = Movie(
0,
"Title",
"Overview",
"01/01/2025",
"",
"",
"ES",
"Title",
5.0,
7.0,
false)
}
This is my DetailViewModel:
class DetailViewModel(
private val movieId: Int, private val findMovieById: FindMovieById,
private val toggleMovieFavorite: ToggleMovieFavorite,
uiDispatcher: CoroutineDispatcher) : ScopedViewModel(uiDispatcher) {
private val _movie = MutableLiveData<Movie>()
val movie: LiveData<Movie> get() = _movie
init {
launch {
_movie.value = findMovieById.invoke(movieId)
}
}
fun onFavoriteClicked() {
launch {
movie.value?.let {
_movie.value = toggleMovieFavorite.invoke(it)
}
}
}
}
And my use case ToggleMovieFavorite:
class ToggleMovieFavorite(private val moviesRepository: MoviesRepository) {
suspend fun invoke(movie: Movie): Movie = with(movie) {
copy(favorite = !favorite).also { moviesRepository.update(it) }
}
}
Thank you so much for your help guys!!!
i thougt mockito does not invoke your init method on viewmodel, you should put your declaration of vm on each #test instead of #Before since method findMovieById called at init, right before the function is mocked.
I'm doing a polling every few seconds and returns a Channel with a Result. My problem is how to test it.
Here is my viewmodel code:
#ExperimentalCoroutinesApi
fun fetchInfo() {
viewModelScope.launch {
channel = fetchInfoUseCase("1")
channel.consumeEach {
viewAction.postValue(ViewAction.UPDATE)
}
}
}
My unit test code:
#ExperimentalCoroutinesApi
class MyViewModelTest {
#get:Rule
val instantTask = InstantTaskExecutorRule()
#get:Rule
val coroutinesTestRule = CoroutinesTestRule()
private val fetchInfo = mockk<FetchInfoUseCase>()
private val channel = Channel<Result<MyModel, MyError>>()
private val viewModel = MyViewModel(fetchInfo)
#Test
fun dispatchFunction_viewActionUpdate() {
prepareScenario()
viewModel.fetchInfo()
coVerify(exactly = 1) { fetchInfo(any()) }
assertEquals(
viewModel.viewAction.value,
ViewAction.UPDATE
)
}
private fun prepareScenario() {
coEvery { fetchInfo(any()) } returns channel
}
}
My in my test, viewModel.viewAction.value is always null.
How can I validate that the viewAction is being called and the value is ViewAction.UPDATE?