Android: Updating database from viewmodel and unit testing it - android

I have a method in my viewmodel that resets rows in the database.
fun resetScores() {
viewModelScope.launch {
for(player in players){
player.level = 1
player.score = 0
playerDao.updatePlayer(player) // updates the DB
}
}
}
var players = mutableListOf<Player>() -- > players is a mutable list
I have this unit test for testing this method
#Test
fun testResetScores() {
val context = ApplicationProvider.getApplicationContext<Context>()
val viewModel = PlayerViewModel(Phase10DataBase.getDatabase(context).playerDao)
viewModel.players = mutableListOf(Player(1,"Player1",5,100),
Player(2,"Player2",5,100),
Player(3,"Player3",5,100))
assertEquals(viewModel.players.get(1).score, 100)
viewModel.resetScores()
assertEquals(viewModel.players.get(1).score, 100)
}
And this test pass whereas I expect it to fail.
Looks like the test is not waiting for the async DB operation to be finished before calling the assert.
What is the right way to test it or should the actual code resetScore need to be updated?

Use runBlocking it blocks until the coroutine is finished
#Test
fun testResetScores() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val viewModel = PlayerViewModel(Phase10DataBase.getDatabase(context).playerDao)
viewModel.players = mutableListOf(Player(1,"Player1",5,100),
Player(2,"Player2",5,100),
Player(3,"Player3",5,100))
assertEquals(viewModel.players.get(1).score, 100)
viewModel.resetScores()
assertEquals(viewModel.players.get(1).score, 100)
}

Related

Use shared flow with Mockito in Test

Im currently trying to write test for my method that use shared flow object and get data from it. the problem is when in trying to set method return with mockito, shared flow not emit anything and after 1 minute test result is fails.
My test:
#ExperimentalCoroutinesApi
#Test
fun `when response body have error in request login`() = runBlockingTest {
runCurrent()
Mockito.`when`(webSocketClient.isConnect()).thenReturn(true)
Mockito.`when`(mapper.createRPC(userLoginObject)).thenReturn(rpc)
Mockito.`when`(requestManager.sendRequest(rpc)).thenReturn(userLoginFlow)
userLoginFlow.emit(errorObject)
loginServiceImpl.requestLogin(userLoginObject).drop(1).collectLatest {
assert(it == errorObject)
}
}
My method
override fun requestLogin(userLoginObject: BaseDomain): Flow<DataState<BaseDomain>> = flow {
emit(DataState.Loading(ProgressBarState.Loading))
if (webSocketClient.isConnect()) {
requestManager.sendRequest(mapper.createRPC(userLoginObject)!!)?.filterNotNull()?.collectLatest {
if (it is IG_RPC.Error) {
emit(DataState.Error(ErrorObject(it.major, it.minor, it.wait)))
} else if (it is IG_RPC.Res_User_Register) {
val userLoginObject = userLoginObject as UserLoginObject
emit(
DataState.Data(
UserLoginObject(
userName = it.userName,
phoneNumber = userLoginObject.phoneNumber,
userId = it.userId,
authorHash = it.authorHash,
regex = it.codeRegex,
resendCodeDelay = it.resendDelayTime
)
)
)
}
}
} else {
emit(DataState.Error(ErrorObject(-1, -1, 0)))
}
}
Error log:
This job has not completed yet
java.lang.IllegalStateException: This job has not completed yet

Why Room LiveData not returning the values?

I have a problem with Room that return LiveData.
I create Dao with function to returns list of data. I suppose to return as LiveData. But, it doesn't work as expected.
Dao function
#Transaction
#Query("SELECT * FROM AllocationPercentage WHERE id IN (:ids)")
fun getByIds(ids: List<Long>): LiveData<List<AllocationPercentageWithDetails>>
Here is how I observe it inside the ViewModel:
class AllocationViewModel(
private val getAllocationByIdUseCase: GetAllocationByIdUseCase,
private val getDetailByIdUseCase: GetAllocationPercentageByIdUseCase
) : ViewModel() {
var allocationUiState: LiveData<AllocationUiState> = MutableLiveData()
private set
var allocationPercentageUiState: LiveData<List<AllocationPercentageUiState>> = MutableLiveData()
private set
val mediatorLiveData = MediatorLiveData<List<AllocationPercentageUiState>>()
fun getAllocationById(allocationId: Long) = viewModelScope.launch(Dispatchers.IO) {
val result = getAllocationByIdUseCase(allocationId) // LiveData
allocationUiState = Transformations.map(result) {
AllocationUiState(allocation = it.allocation)
}
mediatorLiveData.addSource(result) { allocation ->
Log.d(TAG, "> getAllocationById")
val ids = allocation.percentages.map { percentage -> percentage.id }
val detailResult: LiveData<List<AllocationPercentageWithDetails>> =
getDetailByIdUseCase(ids) // LiveData
allocationPercentageUiState = Transformations.map(detailResult) { details ->
Log.d(TAG, ">> Transform : $details")
details.map {
AllocationPercentageUiState(
id = it.allocationPercentage.id,
percentage = it.allocationPercentage.percentage,
description = it.allocationPercentage.description,
currentProgress = it.allocationPercentage.currentProgress
)
}
}
}
}
}
The allocationPercentageUiState is observed by Fragment.
Log.d(TAG, "observeViewModel: ${it?.size}")
val percentages = it ?: return#observe
setAllocationPercentages(percentages) // update UI
}
allocationViewModel.mediatorLiveData.observe(viewLifecycleOwner) {}
And getDetailByIdUseCase just a function which directly return result from Dao.
class GetAllocationPercentageByIdUseCase(private val repository: AllocationPercentageRepository) {
operator fun invoke(ids: List<Long>): LiveData<List<AllocationPercentageWithDetails>> {
return repository.getAllocationPercentageByIds(ids)
}
}
Any idea why? Thank you.
Combining var with LiveData or MutableLiveData doesn't make sense. It defeats the purpose of using LiveData. If something comes along and observes the original LiveData that you have in that property, it will never receive anything. It will have no way of knowing there's a new LiveData instance it should be observing instead.
I can't exactly tell you how to fix it because your code above is incomplete, so I can't tell what you're trying to do in your mapping function, or whether it is called in some function vs. during ViewModel initialization.

Kotlin coroutines mvvm return value from model

I'm making a crawling logic by using coroutines in Kotlin but i don't know this code is right.
this is model class
suspend fun parseYgosu() : Elements? {
var data:Elements? = null
var x : Deferred<Elements?> = CoroutineScope(Dispatchers.IO).async {
var doc = Jsoup.connect("https://www.ygosu.com/community/real_article").get()
data = doc.select("div.board_wrap tbody tr")
data
}
x.await()
Log.d(TAG, "$data")
return data
}
This code have problems. I do not want it be a suspend function.
And also I want to get data from this function by calling it from repository class.
could you help me?
You can use liveData builder
fun parseYgosu(): LiveData<Elements?> = liveData {
val element = withContext(Dispatchers.IO) {
Jsoup.connect("https://www.ygosu.com/community/real_article")
.get()
.select("div.board_wrap tbody tr")
}
emit(element)
}
and UI side:
// for fragment
viewModel.parseYgosu().observe(viewLifecycleOwner, Observer { element -> ... })
// or for activity
viewModel.parseYgosu().observe(this, Observer { element -> ... })
Sticking with the future Deferred, if you don't want it to be suspend then you can't have await() in it
// Not suspend
fun parseYgosuAsync() = CoroutineScope(Dispatchers.IO).async {
val doc = Jsoup.connect("https://www.ygosu.com/community/real_article").get()
val data = doc.select("div.board_wrap tbody tr")
Log.d(TAG, "$data")
data
}

Mockito's `thenReturn` returns `null` instead of Pair(null."text") when called by tested class and not directly

Testing and Mockito beginner here, so I hope it's just some simple error.
EDIT it's most likely due to the functions being suspend functions
My test is crashing beacause Mockito returns null where it should return non-nullable Pair. In live environment this code works fine (no NPE), but I cannot make the test pass with Mockito.
Troubling mock:
val mks = mock(MyKeyStore::class.java)
`when`(mks.createKeyStore(user,pass)).thenReturn(Pair(null, "userExists"))
MyKeyStore.createKeyStore() returns non-nullable Pair
suspend fun createKeyStore(user: String, ksPass: String): Pair<KeyStore?, String>
mks.createKeyStore() is called by UserRepo.createUser()
UserRepo crashes beacuse the test considers ksResult == null, which by defintion is non-nullable. When I change it to nullable the code doesn't compile at all, so I'm thinking it's related to Mockito setup.
class UserRepo(private val myKeyStore: MyKeyStore) {
suspend fun createUser(user: String, p0: String, p1: String): Pair<Boolean, String> =
withContext(Dispatchers.IO) {
return#withContext if (p0 == p1) {
val ksResult = myKeyStore.createKeyStore(user, p0)
ksResult.first?.let { //line where NPE crash leads
val keyResult = myKeyStore.createKeyDB(user, p0)
keyResult.first?.let { Pair(true, keyResult.second) } ?: run { Pair(false, keyResult.second) }
} ?: run { Pair(false, ksResult.second) }
} else Pair(false, myKeyStore.mPasswordsNotMatching)
}
}
full test
#Test
fun createUserFailDueToUserExisting() = runBlocking() {
val user = "user"
val pass = "pass"
val mks = mock(MyKeyStore::class.java)
`when`(mks.createKeyStore(user,pass)).thenReturn(Pair(null, "userExists"))
println(mks.createKeyStore(user,pass)) // this actually prints the pair correctly
val repo = UserRepo(mks)
val result = repo.createUser(user,pass,pass) // NPE crash, but why?
assertFalse(result.first)
assertTrue(result.second == "userExists")
}
How can I configure the mock to return Pair rather than null?
it's because the funtions are suspend functions, specyfing dispatcher in runBlocking fixed it for me
where I found an answer: testing coroutines in Android
#Test
fun createUserFailDueToUserExisting() = runBlocking(Dispatchers.IO) {
val user = "user"
val pass = "pass"
val mks = mock(MyKeyStore::class.java)
`when`(mks.createKeyStore(user, pass)).thenReturn(Pair(null, "userExists"))
println(mks.createKeyStore(user, pass))
val repo = UserRepo(mks)
val result = repo.createUser(user, pass, pass)
assertFalse(result.first)
assertTrue(result.second == "userExists")
}

How to cancel a running LiveData Coroutine Block

By using LiveData's latest version "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03", I have developed a code for a feature called "Search Products" in the ViewModel using LiveData's new building block (LiveData + Coroutine) that performs a synchronous network call using Retrofit and update different flags (isLoading, isError) in ViewModel accordingly. I am using Transforamtions.switchMap on "query" LiveData so whenever there is a change in "query" from the UI, the "Search Products" code starts its executing using Transformations.switchMap. Every thing is working fine, except that i want to cancel the previous Retrofit Call whenever a change happens in "query" LiveData. Currently i can't see any way to do this. Any help would be appreciated.
class ProductSearchViewModel : ViewModel() {
val completableJob = Job()
private val coroutineScope = CoroutineScope(Dispatchers.IO + completableJob)
// Query Observable Field
val query: MutableLiveData<String> = MutableLiveData()
// IsLoading Observable Field
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
val products: LiveData<List<ProductModel>> = query.switchMap { q ->
liveData(context = coroutineScope.coroutineContext) {
emit(emptyList())
_isLoading.postValue(true)
val service = MyApplication.getRetrofitService()
val response = service?.searchProducts(q)
if (response != null && response.isSuccessful && response.body() != null) {
_isLoading.postValue(false)
val body = response.body()
if (body != null && body.results != null) {
emit(body.results)
}
} else {
_isLoading.postValue(false)
}
}
}
}
You can solve this problem in two ways:
Method # 1 ( Easy Method )
Just like Mel has explained in his answer, you can keep a referece to the job instance outside of switchMap and cancel instantance of that job right before returning your new liveData in switchMap.
class ProductSearchViewModel : ViewModel() {
// Job instance
private var job = Job()
val products = Transformations.switchMap(_query) {
job.cancel() // Cancel this job instance before returning liveData for new query
job = Job() // Create new one and assign to that same variable
// Pass that instance to CoroutineScope so that it can be cancelled for next query
liveData(CoroutineScope(job + Dispatchers.IO).coroutineContext) {
// Your code here
}
}
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
Method # 2 ( Not so clean but self contained and reusable)
Since liveData {} builder block runs inside a coroutine scope, you can use a combination of CompletableDeffered and coroutine launch builder to suspend that liveData block and observe query liveData manually to launch jobs for network requests.
class ProductSearchViewModel : ViewModel() {
private val _query = MutableLiveData<String>()
val products: LiveData<List<String>> = liveData {
var job: Job? = null // Job instance to keep reference of last job
// LiveData observer for query
val queryObserver = Observer<String> {
job?.cancel() // Cancel job before launching new coroutine
job = GlobalScope.launch {
// Your code here
}
}
// Observe query liveData here manually
_query.observeForever(queryObserver)
try {
// Create CompletableDeffered instance and call await.
// Calling await will suspend this current block
// from executing anything further from here
CompletableDeferred<Unit>().await()
} finally {
// Since we have called await on CompletableDeffered above,
// this will cause an Exception on this liveData when onDestory
// event is called on a lifeCycle . By wrapping it in
// try/finally we can use this to know when that will happen and
// cleanup to avoid any leaks.
job?.cancel()
_query.removeObserver(queryObserver)
}
}
}
You can download and test run both of these methods in this demo project
Edit: Updated Method # 1 to add job cancellation on onCleared method as pointed out by yasir in comments.
Retrofit request should be cancelled when parent scope is cancelled.
class ProductSearchViewModel : ViewModel() {
val completableJob = Job()
private val coroutineScope = CoroutineScope(Dispatchers.IO + completableJob)
/**
* Adding job that will be used to cancel liveData builder.
* Be wary - after cancelling, it'll return a new one like:
*
* ongoingRequestJob.cancel() // Cancelled
* ongoingRequestJob.isActive // Will return true because getter created a new one
*/
var ongoingRequestJob = Job(coroutineScope.coroutineContext[Job])
get() = if (field.isActive) field else Job(coroutineScope.coroutineContext[Job])
// Query Observable Field
val query: MutableLiveData<String> = MutableLiveData()
// IsLoading Observable Field
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
val products: LiveData<List<ProductModel>> = query.switchMap { q ->
liveData(context = ongoingRequestJob) {
emit(emptyList())
_isLoading.postValue(true)
val service = MyApplication.getRetrofitService()
val response = service?.searchProducts(q)
if (response != null && response.isSuccessful && response.body() != null) {
_isLoading.postValue(false)
val body = response.body()
if (body != null && body.results != null) {
emit(body.results)
}
} else {
_isLoading.postValue(false)
}
}
}
}
Then you need to cancel ongoingRequestJob when you need to. Next time liveData(context = ongoingRequestJob) is triggered, since it'll return a new job, it should run without problems. All you need to left is cancel it where you need to, i.e. in query.switchMap function scope.

Categories

Resources