Use shared flow with Mockito in Test - android

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

Related

Error in kotlin flow doesn't trigger catch in unit test

With migration to kotlin, view model and recent changes in [kotlin test lib][1] I am working on issue with test.
I have a scenario:
request a web resource asynchronously
in case of error put the request in cache and update state with new pending request
All of this with help of kotlin flow and view model.
Scenario works well when executes on emulator, but fails when I run test for it. The issue is catch block of flow has not been triggered when error has thrown in flow.
Here is the code:
fun mintToken(to: String, value: Value, uri: String) {
logger.d("[start] mintToken()")
viewModelScope.launch {
repository.mintToken(to, value, uri)
.catch { it ->
if (it is TransactionException
&& it.message!!.contains("Transaction receipt was not generated after 600 seconds for transaction")) {
cacheRepository.createChainTx(to, value, uri) // TODO consider always put in pending cache and remove after it confirms as succeeded
val txReceipt = TransactionReceipt()
txReceipt.transactionHash = ""
emit(Response.Data(txReceipt))
} else {
emit(Response.Error.Exception(it))
}
}
.flowOn(Dispatchers.IO)
.collect {
logger.d(it.toString())
when (it) {
is Response.Data -> {
if (it.data.transactionHash.isEmpty()) {
state.update {
it.copy(
status = Status.MINT_TOKEN,
pendingTx = it.pendingTx + Transaction(to, value, uri)
)
}
}
}
is Response.Error.Message -> {
val errorMsg = "Something went wrong on mint a token with error ${it.msg}"
logger.d(errorMsg)
state.update {
val newErrors = it.errors + "Something went wrong on mint a token with error ${errorMsg}"
it.copy(status = Status.MINT_TOKEN, errors = newErrors)
}
}
is Response.Error.Exception -> {
logger.e("Something went wrong on mint a token ${to}, ${value}, ${uri}", it.error)
state.update {
val newErrors = it.errors + "Something went wrong on mint a token ${to}, ${value}, ${uri}"
it.copy(status = Status.MINT_TOKEN, errors = newErrors)
}
}
}
}
}
logger.d("[end] mintToken()")
}
#Throws(TransactionException::class)
override fun mintToken(to: String, value: Value, uri: String): Flow<Response<TransactionReceipt>> {
return flow {
throw TransactionException(
"Transaction receipt was not generated after 600 seconds for transaction",
"")
}
}
Test code for this is:
#get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
// Set the main coroutines dispatcher for unit testing.
#ExperimentalCoroutinesApi
#get:Rule
var mainCoroutineRule = MainCoroutineRule()
private lateinit var subj: WalletViewModel
#Test
fun `when mintToken() is called with correct values, timeout exception is returned and pending tx are updated with new value`() = runTest {
val to = "0x6f1d841afce211dAead45e6109895c20f8ee92f0"
val url = "https://google.com"
val testValue = Value(
"Software Development",
BigInteger.valueOf(1000L),
BigInteger.valueOf(2000L),
false,
BigInteger.valueOf(0)
)
subj.mintToken(to, testValue, url)
assertThat(
"There is no pending transaction after mint a new token with timeout error",
subj.uiState.value.pendingTx.isNotEmpty()
)
}
Test code differs from dev code by replacing dispatcher in MainCoroutineRule and using kotlin construction runTest {}. How does it affect this case? Does issue case lays in some other place?
[1]: https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md

Android unit testing for flow with emitAll

I have a function that return flow by emitAll
fun handle(actions: MoviesActions): Flow<MoviesStates> = flow {
when (actions) {
is MoviesActions.LoadMovies -> {
emit(MoviesStates.Loading)
emitAll(moviesUseCase.execute())
}
}
}
And this the use case function
suspend fun execute(): Flow<MoviesStates> = flow {
combine(f1, f2) { state1: MoviesStates, state2: MoviesStates ->
// some code
}.collect {
emit(it)
}
}
No problem in testing the first emission MoviesStates.Loading, the problem is when I try to test the flow which return from usecase by emitAll emitAll(moviesUseCase.execute()), the test fails and I got this result
java.util.NoSuchElementException: Expected at least one element
this is my unit test
#Test
fun testLoadMovies() = runBlocking {
whenever(useCase.execute()).thenReturn(flow {
MoviesStates.EmptyList
})
val actual = viewModel.handle(MoviesActions.LoadMovies).drop(1).first()
val expected = MoviesStates.EmptyList
assertEquals(actual, expected)
}
So How can I test it correctly?
Thanks to gpunto , this is the solution he suggested
#Test
fun testLoadMovies() = runTest {
whenever(useCase.execute()).thenReturn(flow {
MoviesStates.EmptyList
})
useCase.execute().collectLatest { states ->
val actual = viewModel.handle(MoviesActions.LoadMovies).drop(1).first()
val expected = states
assertEquals(expected, actual)
}
}

Kotlin Coroutines: Issue with job-scheduling.(invokeOnCompletion)

I am fairly new to this kotlin-coroutine thing and i have an issue with job-scheduling.In this code below, first i fetch topic names from user's cache in the fragment.(topicsList)
And then, i need to fetch these topics from API one by one. What i want to do is loop through the topicsList, make a request for each topic and get all the responses once at the completion of all requests. In order to achieve that, in getEverything() method(which fires up a request), i am adding the responses into an arraylist for every time.(responseList)
In for loop, i am firing up all the requests. After the completion of the job, job.invokeOnCompletion{} is called and i set my liveData to responseList. However, this approach doesn't work. Problem is, i am updating the liveData before the setting the responseList. I don't know how can it be possible. Could anybody help me about this?
Here is my CoroutineScope in myFragment:
val topicsList = dataMap["topics"] // GOT THE TOPICS
topicsList?.let {
var job: Job
CoroutineScope(Dispatchers.Main).launch {
job = launch {
for (topic in topicsList) {
mViewModel.getEverything(topic, API_KEY)
}
}
job.join()
job.invokeOnCompletion {
mViewModel.updateLiveData()
}
}
} ?: throw Exception("NULL")
getEverything() method in viewModel:
suspend fun getEverything(topic: String, apiKey: String) {
viewModelScope.launch {
_isLoading.value = true
withContext(Dispatchers.IO) {
val response = api.getEverything(topic, apiKey)
withContext(Dispatchers.Main) {
if (response.isSuccessful) {
if (response.body() != null) {
responseList.add(response.body()!!)
println("Response is successful: ${response.body()!!}")
_isLoading.value = false
_isError.value = false
}
}
else {
Log.d(TAG, "getEverything: ${response.errorBody()}")
_isError.value = true
_isLoading.value = false
}
}
}
}
}
And, updateLiveData method:
fun updateLiveData() {
_newsResponseList.value = responseList
println("response list : ${responseList.size}")
responseList.clear()
}
And this is how it looks in the logs: Logs
Logs for you who cannot open the image :
I/System.out: response list : 0
I/System.out: Response is successful: NewsResponse(articleList=[Article(source=Source(id=wired, ...
I/System.out: Response is successful: NewsResponse(articleList=[Article(source=Source(id=techcrunch, ...
I/System.out: Response is successful: NewsResponse(articleList=[Article(source=Source(id=wired, ...
I/System.out: Response is successful: NewsResponse(articleList=[Article(source=Source(id=the-verge, ...
Btw data is fetched without an error and its correct. I've no issue with that.
The issue is that getEverything uses launch to create a background job, then returns before it knows the job is complete.
To fix this, have getEverything return the data directly:
suspend fun getEverything(topic: String, apiKey: String): Response? {
_isLoading.value = true
val response = withContext(Dispatchers.IO) {
api.getEverything(topic, apiKey)
}
_isLoading.value = false
return response.takeIf { it.isSuccessful }?.body()?.let { body ->
println("Response is successful: $body")
}.also {
_isError.value = it == null
}
}
In your Fragment, request the results and assign them:
lifecycleScope.launch {
_responseList.value = topicsList.mapNotNull { topic ->
model.getResponse(topic, apiKey)
}
}

RxJava retryWhen with updated value + concurrency

I'd like to verify two things:
if the way I use retryWhen is correct or not. Basically, when an exception is caught, I want to get an updated value and rerun the sequence.
if another function also needs value, how to make it waits for updateValue to complete in the first instance? I've played with .share() and RxReplayingShare but I'm not sure how to use those properly.
val value = 0
#Test
fun test() {
executeFunction()
.retryWhen { errors -> errors.flatMap { error ->
if (error is WrongValue) {
updateValue()
.doOnSuccess { value = it }
.toObservable()
} else {
Observable.error(error)
}
}
}
}
fun executeFunction(): Single<Int> =
if (value == 0) {
Single.error(WrongValue())
} else {
Single.just(value)
}
fun updateValue(): Single<Int> = Single.just(1)

Firebase realtime snapshot listener using Coroutines

I want to be able to listen to realtime updates in Firebase DB's using Kotlin coroutines in my ViewModel.
The problem is that whenever a new message is created in the collection my application freezes and won't recover from this state. I need to kill it and restart app.
For the first time it passes and I can see the previous messages on the UI. This problem happens when SnapshotListener is called for 2nd time.
My observer() function
val channel = Channel<List<MessageEntity>>()
firestore.collection(path).addSnapshotListener { data, error ->
if (error != null) {
channel.close(error)
} else {
if (data != null) {
val messages = data.toObjects(MessageEntity::class.java)
//till this point it gets executed^^^^
channel.sendBlocking(messages)
} else {
channel.close(CancellationException("No data received"))
}
}
}
return channel
That's how I want to observe messages
launch(Dispatchers.IO) {
val newMessages =
messageRepository
.observer()
.receive()
}
}
After I replacing sendBlocking() with send() I am still not getting any new messages in the channel. SnapshotListener side is executed
//channel.sendBlocking(messages) was replaced by code bellow
scope.launch(Dispatchers.IO) {
channel.send(messages)
}
//scope is my viewModel
How to observe messages in firestore/realtime-dbs using Kotlin coroutines?
I have these extension functions, so I can simply get back results from the query as a Flow.
Flow is a Kotlin coroutine construct perfect for this purposes.
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/
#ExperimentalCoroutinesApi
fun CollectionReference.getQuerySnapshotFlow(): Flow<QuerySnapshot?> {
return callbackFlow {
val listenerRegistration =
addSnapshotListener { querySnapshot, firebaseFirestoreException ->
if (firebaseFirestoreException != null) {
cancel(
message = "error fetching collection data at path - $path",
cause = firebaseFirestoreException
)
return#addSnapshotListener
}
offer(querySnapshot)
}
awaitClose {
Timber.d("cancelling the listener on collection at path - $path")
listenerRegistration.remove()
}
}
}
#ExperimentalCoroutinesApi
fun <T> CollectionReference.getDataFlow(mapper: (QuerySnapshot?) -> T): Flow<T> {
return getQuerySnapshotFlow()
.map {
return#map mapper(it)
}
}
The following is an example of how to use the above functions.
#ExperimentalCoroutinesApi
fun getShoppingListItemsFlow(): Flow<List<ShoppingListItem>> {
return FirebaseFirestore.getInstance()
.collection("$COLLECTION_SHOPPING_LIST")
.getDataFlow { querySnapshot ->
querySnapshot?.documents?.map {
getShoppingListItemFromSnapshot(it)
} ?: listOf()
}
}
// Parses the document snapshot to the desired object
fun getShoppingListItemFromSnapshot(documentSnapshot: DocumentSnapshot) : ShoppingListItem {
return documentSnapshot.toObject(ShoppingListItem::class.java)!!
}
And in your ViewModel class, (or your Fragment) make sure you call this from the right scope, so the listener gets removed appropriately when the user moves away from the screen.
viewModelScope.launch {
getShoppingListItemsFlow().collect{
// Show on the view.
}
}
What I ended up with is I used Flow which is part of coroutines 1.2.0-alpha-2
return flowViaChannel { channel ->
firestore.collection(path).addSnapshotListener { data, error ->
if (error != null) {
channel.close(error)
} else {
if (data != null) {
val messages = data.toObjects(MessageEntity::class.java)
channel.sendBlocking(messages)
} else {
channel.close(CancellationException("No data received"))
}
}
}
channel.invokeOnClose {
it?.printStackTrace()
}
}
And that's how I observe it in my ViewModel
launch {
messageRepository.observe().collect {
//process
}
}
more on topic https://medium.com/#elizarov/cold-flows-hot-channels-d74769805f9
Extension function to remove callbacks
For Firebase's Firestore database there are two types of calls.
One time requests - addOnCompleteListener
Realtime updates - addSnapshotListener
One time requests
For one time requests there is an await extension function provided by the library org.jetbrains.kotlinx:kotlinx-coroutines-play-services:X.X.X. The function returns results from addOnCompleteListener.
For the latest version, see the Maven Repository, kotlinx-coroutines-play-services.
Resources
Using Firebase on Android with Kotlin Coroutines by Joe Birch
Using Kotlin Extension Functions and Coroutines with Firebase by Rosário Pereira Fernandes
Realtime updates
The extension function awaitRealtime has checks including verifying the state of the continuation in order to see whether it is in isActive state. This is important because the function is called when the user's main feed of content is updated either by a lifecycle event, refreshing the feed manually, or removing content from their feed. Without this check there will be a crash.
ExtenstionFuction.kt
data class QueryResponse(val packet: QuerySnapshot?, val error: FirebaseFirestoreException?)
suspend fun Query.awaitRealtime() = suspendCancellableCoroutine<QueryResponse> { continuation ->
addSnapshotListener({ value, error ->
if (error == null && continuation.isActive)
continuation.resume(QueryResponse(value, null))
else if (error != null && continuation.isActive)
continuation.resume(QueryResponse(null, error))
})
}
In order to handle errors the try/catch pattern is used.
Repository.kt
object ContentRepository {
fun getMainFeedList(isRealtime: Boolean, timeframe: Timestamp) = flow<Lce<PagedListResult>> {
emit(Loading())
val labeledSet = HashSet<String>()
val user = usersDocument.collection(getInstance().currentUser!!.uid)
syncLabeledContent(user, timeframe, labeledSet, SAVE_COLLECTION, this)
getLoggedInNonRealtimeContent(timeframe, labeledSet, this)
}
// Realtime updates with 'awaitRealtime' used
private suspend fun syncLabeledContent(user: CollectionReference, timeframe: Timestamp,
labeledSet: HashSet<String>, collection: String,
lce: FlowCollector<Lce<PagedListResult>>) {
val response = user.document(COLLECTIONS_DOCUMENT)
.collection(collection)
.orderBy(TIMESTAMP, DESCENDING)
.whereGreaterThanOrEqualTo(TIMESTAMP, timeframe)
.awaitRealtime()
if (response.error == null) {
val contentList = response.packet?.documentChanges?.map { doc ->
doc.document.toObject(Content::class.java).also { content ->
labeledSet.add(content.id)
}
}
database.contentDao().insertContentList(contentList)
} else lce.emit(Error(PagedListResult(null,
"Error retrieving user save_collection: ${response.error?.localizedMessage}")))
}
// One time updates with 'await' used
private suspend fun getLoggedInNonRealtimeContent(timeframe: Timestamp,
labeledSet: HashSet<String>,
lce: FlowCollector<Lce<PagedListResult>>) =
try {
database.contentDao().insertContentList(
contentEnCollection.orderBy(TIMESTAMP, DESCENDING)
.whereGreaterThanOrEqualTo(TIMESTAMP, timeframe).get().await()
.documentChanges
?.map { change -> change.document.toObject(Content::class.java) }
?.filter { content -> !labeledSet.contains(content.id) })
lce.emit(Lce.Content(PagedListResult(queryMainContentList(timeframe), "")))
} catch (error: FirebaseFirestoreException) {
lce.emit(Error(PagedListResult(
null,
CONTENT_LOGGED_IN_NON_REALTIME_ERROR + "${error.localizedMessage}")))
}
}
This is working for me:
suspend fun DocumentReference.observe(block: suspend (getNextSnapshot: suspend ()->DocumentSnapshot?)->Unit) {
val channel = Channel<Pair<DocumentSnapshot?, FirebaseFirestoreException?>>(Channel.UNLIMITED)
val listenerRegistration = this.addSnapshotListener { value, error ->
channel.sendBlocking(Pair(value, error))
}
try {
block {
val (value, error) = channel.receive()
if (error != null) {
throw error
}
value
}
}
finally {
channel.close()
listenerRegistration.remove()
}
}
Then you can use it like:
docRef.observe { getNextSnapshot ->
while (true) {
val value = getNextSnapshot() ?: continue
// do whatever you like with the database snapshot
}
}
If the observer block throws an error, or the block finishes, or your coroutine is cancelled, the listener is removed automatically.

Categories

Resources