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
Related
Consider the following code:
sealed interface State {
object Loading : State
data class Content(val someString: String) : State
}
class SomeViewModel(getSomeStringUseCase: GetSomeStringUseCase) : ViewModel() {
private val _someString = MutableLiveData<State>()
val someString: LiveData<State> = _someString
init {
_someString.value = State.Loading
viewModelScope.launch {
_someString.value = State.Content(getSomeStringUseCase())
}
}
}
In a unit test it's pretty simple to test and assert the last value emitted by someString, however, if I want to assert all values emitted it gets more complicated because I can't subscribe to someString before SomeViewModel is initialized and if I do the subscription right after the initialization it is too late and the values were already emitted:
class SomeViewModelTest {
#MockK
private lateinit var getSomeStringUseCase: GetSomeStringUseCase
private lateinit var viewModel: SomeViewModel
#Before
fun setUp() {
coEvery { getSomeStringUseCase() } returns "Some String!"
viewModel = SomeViewModel(getSomeStringUseCase)
}
// This test fails
#Test
fun test() {
val observer = mockk<Observer<State>>(relaxed = true)
viewModel.someString.observeForever(observer)
verifyOrder {
observer.onChanged(State.Loading)
observer.onChanged(State.Content("Some String!"))
}
}
}
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 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)
}
}
I have been trying, without success, to do some UI tests on Android.
My app follows the MVVM architecture and uses Koin for DI.
I followed this tutorial to properly set up a UI test for a Fragment with Koin, MockK and Kakao.
I created the custom rule for injecting mocks, setup the ViewModel, and on the #Before call, run the expected answers and returns with MockK. The problem is that, even when the fragment's viewmodel's LiveData object is the same as the testing class's LiveData object, the Observer's onChange is never triggered on the Fragment.
I run the test with the debugger and it seems the LiveData functions and MockK's answers are properly called. The logs show that the value hold by the LiveData objects is the same. The lifecycle of the Fragment when the test is running is Lifecycle.RESUMED. So why is the Observer's onChange(T) not being triggered?
The custom rule:
#VisibleForTesting(otherwise = VisibleForTesting.NONE)
abstract class FragmentTestRule<F : Fragment> :
ActivityTestRule<FragmentActivity>(FragmentActivity::class.java, true, true) {
override fun afterActivityLaunched() {
super.afterActivityLaunched()
activity.runOnUiThread {
val fm = activity.supportFragmentManager
val transaction = fm.beginTransaction()
transaction.replace(
android.R.id.content,
createFragment()
).commit()
}
}
override fun beforeActivityLaunched() {
super.beforeActivityLaunched()
val app = InstrumentationRegistry.getInstrumentation()
.targetContext.applicationContext as VideoWorldTestApp
app.injectModules(getModules())
}
protected abstract fun createFragment(): F
protected abstract fun getModules(): List<Module>
fun launch() {
launchActivity(Intent())
}
}
#VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <F : Fragment> createRule(fragment: F, vararg module: Module): FragmentTestRule<F> =
object : FragmentTestRule<F>() {
override fun createFragment(): F = fragment
override fun getModules(): List<Module> = module.toList()
}
My test App:
#VisibleForTesting(otherwise = VisibleForTesting.NONE)
class VideoWorldTestApp: Application(){
companion object {
lateinit var instance: VideoWorldTestApp
}
override fun onCreate() {
super.onCreate()
instance = this
startKoin {
if (BuildConfig.DEBUG) androidLogger(Level.DEBUG) else EmptyLogger()
androidContext(this#VideoWorldTestApp)
modules(emptyList())
}
Timber.plant(Timber.DebugTree())
}
internal fun injectModules(modules: List<Module>) {
loadKoinModules(modules)
}
}
The custom test runner:
class CustomTestRunner: AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(cl, VideoWorldTestApp::class.java.name, context)
}
}
The test:
#RunWith(AndroidJUnit4ClassRunner::class)
class HomeFragmentTest {
private val twitchViewModel: TwitchViewModel = mockk(relaxed = true)
private val userData = MutableLiveData<UserDataResponse>()
private val fragment = HomeFragment()
#get:Rule
var fragmentRule = createRule(fragment, module {
single(override = true) {
twitchViewModel
}
})
#get:Rule
var countingTaskExecutorRule = CountingTaskExecutorRule()
#Before
fun setup() {
val userResponse: UserResponse = mockk()
every { userResponse.displayName } returns "Rubius"
every { userResponse.profileImageUrl } returns ""
every { userResponse.description } returns "Soy streamer"
every { userResponse.viewCount } returns 5000
every { twitchViewModel.userData } returns userData as LiveData<UserDataResponse>
every { twitchViewModel.getUserByInput(any()) }.answers {
userData.value = UserDataResponse(listOf(userResponse))
}
}
#Test //This one is passing
fun testInitialViewState() {
onScreen<HomeScreen> {
streamerNameTv.containsText("")
streamerCardContainer.isVisible()
nameInput.hasEmptyText()
progressBar.isGone()
}
}
#Test //This one is failing
fun whenWritingAName_AndPressingTheImeAction_AssertTextChanges() {
onScreen<HomeScreen> {
nameInput.typeText("Rubius")
//nameInput.pressImeAction()
searchBtn.click()
verify { twitchViewModel.getUserByInput(any()) } //This passes
countingTaskExecutorRule.drainTasks(5, TimeUnit.SECONDS)
streamerNameTv.hasText("Rubius") //Throws exception
streamerDescp.hasText("Soy streamer")
streamerCount.hasText("Views: ${5000.formatInt()}}")
}
}
}
The fragment being tested:
class HomeFragment : BaseFragment<FragmentHomeBinding>(R.layout.fragment_home) {
override val bindingFunction: (view: View) -> FragmentHomeBinding
get() = FragmentHomeBinding::bind
val twitchViewModel: TwitchViewModel by sharedViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
twitchViewModel.getUserClips("")
binding.nameInput.setOnEditorActionListener { _, actionId, _ ->
if(actionId == EditorInfo.IME_ACTION_SEARCH) {
twitchViewModel.getUserByInput(binding.nameInput.text.toString())
hideKeyboard()
return#setOnEditorActionListener true
}
return#setOnEditorActionListener false
}
binding.searchBtn.setOnClickListener {
twitchViewModel.getUserByInput(binding.nameInput.text.toString() ?: "")
hideKeyboard()
}
twitchViewModel.userData.observe(viewLifecycleOwner, Observer { data ->
if (data != null && data.dataList.isNotEmpty()){
binding.streamerCard.setOnClickListener {
findNavController().navigate(R.id.action_homeFragment_to_clipsFragment)
}
val streamer = data.dataList[0]
Picasso.get()
.load(streamer.profileImageUrl)
.into(binding.profileIv)
binding.streamerLoginTv.text = streamer.displayName
binding.streamerDescpTv.text = streamer.description
binding.streamerViewCountTv.text = "Views: ${streamer.viewCount.formatInt()}"
}
else {
binding.streamerCard.setOnClickListener { }
}
})
twitchViewModel.errorMessage.observe(viewLifecycleOwner, Observer { msg ->
showSnackbar(msg)
})
twitchViewModel.progressVisibility.observe(viewLifecycleOwner, Observer { visibility ->
binding.progressBar.visibility = visibility
binding.cardContent.visibility =
if(visibility == View.VISIBLE)
View.GONE
else
View.VISIBLE
})
}
}
The ViewModel:
class TwitchViewModel(private val repository: TwitchRepository): BaseViewModel() {
private val _userData = MutableLiveData<UserDataResponse>()
val userData = _userData as LiveData<UserDataResponse>
private val _userClips = MutableLiveData<UserClipsResponse?>()
val userClips = _userClips as LiveData<UserClipsResponse?>
init {
viewModelScope.launch {
repository.authUser(this#TwitchViewModel)
}
}
fun currentUserId() = userData.value?.dataList?.get(0)?.id ?: ""
fun clipsListExists() = userClips.value != null
fun getUserByInput(input: String){
viewModelScope.launch {
_progressVisibility.value = View.VISIBLE
_userData.value = repository.getUserByName(input, this#TwitchViewModel)
_progressVisibility.value = View.GONE
}
}
/**
* #param userId The ID of the Streamer whose clips are gonna fetch. If null, resets
* If empty, sets the [userClips] value to null.
*/
fun getUserClips(userId: String){
if(userId.isEmpty()) {
_userClips.postValue(null)
return
}
if(userId == currentUserId() && _userClips.value != null) {
_userClips.postValue(_userClips.value)
return
}
viewModelScope.launch {
_userClips.value = repository.getUserClips(userId, this#TwitchViewModel)
}
}
}
When running the test with the normal ActivityRule and launching the Activity as it were a normal launch, the observers are triggering successfully.
I'm using a relaxed mock to avoid having to mock all functions and variables.
Finally found the problem and the solution with the debugger. Apparently, the #Before function call runs after the ViewModel is injected into the fragment, so even if the variables pointed to the same reference, mocked answer where executing only in the test context, not in the android context.
I changed the ViewModel initialization to the module scope like this:
#get:Rule
val fragmentRule = createRule(fragment, module {
single(override = true) {
makeMocks()
val twitchViewModel = mockViewModel()
twitchViewModel
}
})
private fun makeMocks() {
mockkStatic(Picasso::class)
}
private fun mockViewModel(): TwitchViewModel {
val userData = MutableLiveData<UserDataResponse>()
val twitchViewModel = mockk<TwitchViewModel>(relaxed = true)
every { twitchViewModel.userData } returns userData
every { twitchViewModel.getUserByInput("Rubius") }.answers {
updateUserDataLiveData(userData)
}
return twitchViewModel
}
And the Observer inside the Fragment got called!
Maybe it's not related, but I could not rebuild the gradle project if I have mockk(v1.10.0) as a testImplementation and as a debugImplementation.
I have an activity to perform rest API everytime it opened and i use MVVM pattern for this project. But with this snippet code i failed to get updated everytime i open activity. So i debug all my parameters in every line, they all fine the suspect problem might when apiService.readNewsAsync(param1,param2) execute, my postValue did not update my resulRead parameter. There were no crash here, but i got result which not updated from result (postValue). Can someone explain to me why this happened?
Here what activity looks like
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DataBindingUtil.setContentView<ActivityReadBinding>(this,
R.layout.activity_read).apply {
this.viewModel = readViewModel
this.lifecycleOwner = this#ReadActivity
}
readViewModel.observerRead.observe(this, Observer {
val sukses = it.isSuccess
when{
sukses -> {
val data = it.data as Read
val article = data.article
//Log.d("-->", "${article.toString()}")
}
else -> {
toast("ada error ${it.msg}")
Timber.d("ERROR : ${it.msg}")
}
}
})
readViewModel.getReadNews()
}
Viewmodel
var observerRead = MutableLiveData<AppResponse>()
init {
observerRead = readRepository.observerReadNews()
}
fun getReadNews() {
// kanal and guid i fetch from intent and these value are valid
loadingVisibility = View.VISIBLE
val ok = readRepository.getReadNews(kanal!!, guid!!)
if(ok){
loadingVisibility = View.GONE
}
}
REPOSITORY
class ReadRepositoryImpl private constructor(private val newsdataDao: NewsdataDao) : ReadRepository{
override fun observerReadNews(): MutableLiveData<AppResponse> {
return newsdataDao.resultRead
}
override fun getReadNews(channel: String, guid: Int) = newsdataDao.readNews(channel, guid)
companion object{
#Volatile private var instance: ReadRepositoryImpl? = null
fun getInstance(newsdataDao: NewsdataDao) = instance ?: synchronized(this){
instance ?: ReadRepositoryImpl(newsdataDao).also {
instance = it
}
}
}
}
MODEL / DATA SOURCE
class NewsdataDao {
private val apiService = ApiClient.getClient().create(ApiService::class.java)
var resultRead = MutableLiveData<AppResponse>()
fun readNews(channel: String, guid: Int): Boolean{
GlobalScope.launch {
val response = apiService.readNewsAsync(Constants.API_TOKEN, channel, guid.toString()).await()
when{
response.isSuccessful -> {
val res = response.body()
val appRes = AppResponse(true, "ok", res!!)
resultRead.postValue(appRes)
}
else -> {
val appRes = AppResponse(false, "Error: ${response.message()}", null)
resultRead.postValue(appRes)
}
}
}
return true
}
}
Perhaps this activity is not getting stopped.
Check this out:
When you call readViewModel.getReadNews() in onCreate() your activity is created once, only if onStop is called will it be created again.