I'm aware that there are a couple of topics on this but none of them solve my issue.
I'm trying to test an implementation of NetworkBoundResource.
inline fun <ResultType, RequestType, ErrorType> networkBoundResource(
crossinline query: () -> Flow<ResultType>,
crossinline fetch: suspend () -> Response<RequestType>,
crossinline saveFetchResult: suspend (RequestType) -> Unit,
crossinline onFetchFailed: (Response<*>?, Throwable?) -> ErrorType? = { _, _ -> null },
crossinline shouldFetch: (ResultType) -> Boolean = { true },
coroutineDispatcher: CoroutineDispatcher
) = flow<Resource<ResultType, ErrorType>> {
val data = query().first()
emit(Resource.Success(data))
if (shouldFetch(data)) {
val fetchResponse = safeApiCall { fetch() }
val fetchBody = fetchResponse.body()
if (fetchBody != null) {
saveFetchResult(fetchBody)
}
if (!fetchResponse.isSuccessful) {
emit(Resource.Error(onFetchFailed(fetchResponse, null)))
} else {
query().map { emit(Resource.Success(it)) }
}
}
}.catch { throwable ->
emit(Resource.Error(onFetchFailed(null, throwable)))
}.flowOn(coroutineDispatcher)
This works as expected in my use case in production code.
override suspend fun getCategories() = networkBoundResource(
query = {
categoryDao.getAllAsFlow().map { categoryMapper.categoryListFromDataObjectList(it) }
},
fetch = {
categoryServices.getCategories()
},
onFetchFailed = { errorResponse, _ ->
categoryMapper.toError(errorResponse)
},
saveFetchResult = { response ->
// Clear the old items and add the new ones
categoryDao.clearAll()
categoryDao.insertAll(categoryMapper.toDataObjectList(response.data))
},
coroutineDispatcher = dispatchProvider.IO
)
I have my test setup like this (using turbine for flow testing).
#OptIn(ExperimentalCoroutinesApi::class)
class NetworkBoundResourceTests {
data class ResultType(val data: String)
sealed class RequestType {
object Default : RequestType()
}
sealed class ErrorType {
object Default : RequestType()
}
private val dispatchProvider = TestDispatchProviderImpl()
#Test
fun `Test`() = runTest {
val resource = networkBoundResource(
query = { flowOf(ResultType(data = "")) },
fetch = { Response.success(RequestType.Default) },
saveFetchResult = { },
onFetchFailed = { _, _ -> ErrorType.Default },
coroutineDispatcher = dispatchProvider.IO
)
resource.test {
}
}
}
The coroutine dispatcher is set to unconfined through DI/Test dispatcher.
I want to test that;
Emitting first data from query, then query is updated and new data from saveFetchResult then query().map { emit(Resource.Success(it)) } emits the updated data from that save result.
I think I need to do something around a spyk on my flow with MockK but I can't seem to figure it out. query() will always return the same flow of data as it's mocked to do so if I awaitItem() again it returns the same data (as it should) as that's what the mock is setup for.
I've found a way to test this. Not exactly how I imagined it in my head.
#Test
fun `Given should fetch is true and fetch throws exception, When retrieving data, Then cached items emitted and error item after`() =
runTest {
val saveFetchResultAction = mockk<(() -> Unit)>("Save results action")
val fetchErrorAction = mockk<(() -> ErrorType)>("Fetch error action")
every { fetchErrorAction() } answers { ErrorType }
val fetchRequestAction = mockk<(() -> Response<RequestType>)>("Fetch request action")
coEvery { fetchRequestAction() } throws (Exception(""))
networkBoundResource(
query = { flowOf(ResultType) },
fetch = { fetchRequestAction() },
saveFetchResult = { saveFetchResultAction() },
onFetchFailed = { _, _ -> fetchErrorAction() },
shouldFetch = { true },
coroutineDispatcher = dispatchProvider.IO
).test {
// Assert that we've got the cached item
val cacheItem = awaitItem()
assertThat(cacheItem).isInstanceOf(Resource.Success::class.java)
val errorItem = awaitItem()
assertThat(errorItem).isInstanceOf(Resource.Error::class.java)
awaitComplete()
// Verify order & calls
verifyOrder {
fetchRequestAction()
fetchErrorAction()
}
verify(exactly = 1) { fetchErrorAction() }
verify(exactly = 1) { fetchRequestAction() }
verify(exactly = 0) { saveFetchResultAction() }
}
}
Related
I have 5 api call function in the viewModel that I want to called parallel how can I do this? I put each of the function in the WithContext(Dispachers.IO) but it's not working. I used coroutines flow for calling api.
Note: I used clean architecture pattern and I have single use-case
ViewModel codes:
class MyJobsViewModel constructor(
private val myJobsUseCases: MyJobsUseCases,
private val clientNavigator: ClientNavigator
) : ViewModel(), ClientNavigator by clientNavigator {
private val _state = mutableStateOf(MyJobsState())
val state: State<MyJobsState> get() = _state
private fun getAllJobs(
offset: Int = 0,
limit: Int = 10,
type: JobTypeEnum = JobTypeEnum.ALL
) {
myJobsUseCases.getJobsUseCase.invoke(offset = offset, limit = limit, type = type)
.onEach {
when (it) {
is Resource.Success -> _state.value =
state.value.copy(
isLoading = false,
allJobItems = it.data ?: JobItemsResponse()
)
is Resource.Error -> _state.value =
state.value.copy(
isLoading = false,
error = it.message ?: "An unexpected error occurred"
)
is Resource.Loading -> _state.value = state.value.copy(isLoading = true)
}
}.launchIn(viewModelScope)
}
private fun getActiveJobs(
offset: Int = 0,
limit: Int = 10,
type: JobTypeEnum = JobTypeEnum.ALL
) {
myJobsUseCases.getJobsUseCase.invoke(offset = offset, limit = limit, type = type)
.onEach {
when (it) {
is Resource.Success -> _state.value =
state.value.copy(
isLoading = false,
activeJobItems = it.data ?: JobItemsResponse()
)
is Resource.Error -> _state.value =
state.value.copy(
isLoading = false,
error = it.message ?: "An unexpected error occurred"
)
is Resource.Loading -> _state.value = state.value.copy(isLoading = true)
}
}.launchIn(viewModelScope)
}
}
The best way to parallel multiple calls is to follow structured concurrency principle.
For example, when you need to evaluate the result of 5 independent network requests:
suspend fun fetchA(): Int { /* ... */ }
suspend fun fetchB(): Int { /* ... */ }
suspend fun fetchC(): Int { /* ... */ }
suspend fun fetchD(): Int { /* ... */ }
suspend fun fetchE(): Int { /* ... */ }
suspend fun overallResult(): Int = coroutineScope {
val a = async { fetchA() }
val b = async { fetchB() }
val c = async { fetchC() }
val d = async { fetchD() }
val e = async { fetchE() }
a.await() + b.await() + c.await() + d.await() + e.await()
}
Or when you need to make 5 independent api calls without returning any value:
suspend fun callA() { /* ... */ }
suspend fun callB() { /* ... */ }
suspend fun callC() { /* ... */ }
suspend fun callD() { /* ... */ }
suspend fun callE() { /* ... */ }
suspend fun makeCalls(): Unit = coroutineScope {
launch { callA() }
launch { callB() }
launch { callC() }
launch { callD() }
launch { callE() }
}
Wrapping function in launch or async block produces a new coroutine and executes in parallel.
coroutineScope organizes the area where launch and async can be used. It completes only when every child coroutine is completed.
I know that there are a lot of posts "How to cancel Coroutines Scope" but I couldn't find the answer for my case.
I have an Array of objects that I want to send each of them to Server using Coroutines.
What I need is, if one of my requests returns error, canceling others.
Here is my code:
private fun sendDataToServer(function: () -> Unit) {
LiabilitiesWizardSessionManager.getLiabilityAddedDocuments().let { documents ->
if (documents.isEmpty().not()) {
CoroutineScope(Dispatchers.IO).launch {
documents.mapIndexed { index, docDetail ->
async {
val result = uploadFiles(docDetail)
}
}.map {
var result = it.await()
}
}
} else function.invoke()
}
}
Below is my uploadFiles() function:
private suspend fun uploadFiles(docDetail: DocDetail): ArchiveFileResponse? {
LiabilitiesWizardSessionManager.mCreateLiabilityModel.let { model ->
val file = File(docDetail.fullFilePath)
val crmCode = docDetail.docTypeCode
val desc = docDetail.docTypeDesc
val id = model.commitmentMember?.id
val idType = 1
val createArchiveFileModel = CreateArchiveFileModel(108, desc, id, idType).apply {
this.isLiability = true
this.adaSystem = 3
}
val result = mRepositoryControllerKotlin.uploadFile(file, createArchiveFileModel)
return when (result) {
is ResultWrapper.Success -> {
result.value
}
is ResultWrapper.GenericError -> {
null
}
is ResultWrapper.NetworkError -> {
null
}
}
}
}
I know, I'm missing something.
I'm using the following code in my Android app to load data from a remote source into a local database cache.
inline fun <ResultType, RequestType> networkBoundResource(
crossinline query: () -> Flow<ResultType>,
crossinline fetch: suspend () -> RequestType,
crossinline saveFetchResult: suspend (RequestType) -> Unit,
crossinline onFetchSuccess: () -> Unit = { },
crossinline onFetchFailed: (Throwable) -> Unit = { },
crossinline shouldFetch: (ResultType) -> Boolean = { true }
) = flow {
val data = query().first()
val flow = if (shouldFetch(data)) {
emit(Resource.(data))
try {
// this could take a while, I want to keep getting updates meanwhile
saveFetchResult(fetch())
onFetchSuccess()
query().map { Resource.Success(it) }
} catch (t: Throwable) {
onFetchFailed(t)
query().map { Resource.Error(t, it) }
}
} else {
query().map { Resource.Success(it) }
}
emitAll(flow)
}
The query is a database query that keeps emitting database updates through emitAll until we call this method again.
The problem with this setup is that Resource.Loading only contains a "snapshot" of the current data (first()) and we won't receive any database updates until we get to the end of the try/catch block and call emitAll. But I would like to keep receiving database updates while Loading is still in progress. However, I can't just call emitAll on Resource.Loading because it would block the whole Flow.
Is there a way to call emitAll on Loading and then switch to Success/Error once the try block has finished?
I've only done simple testing on this to validate it, but it looks like you can listen to the query and emit any/all data it propagates in a newly launched coroutine based on the outer Flow's context -- the other work in the function will continue, unblocked. Once that other work is done, the coroutine that's listening to the query can be cancelled. For example:
inline fun <ResultType, RequestType> networkBoundResource(
crossinline query: () -> Flow<ResultType>,
crossinline fetch: suspend () -> RequestType,
crossinline saveFetchResult: suspend (RequestType) -> Unit,
crossinline onFetchFailed: (Throwable) -> Unit = { },
crossinline shouldFetch: (ResultType) -> Boolean = { true }
): Flow<Resource<ResultType>> = flow {
emit(Resource.Loading())
val data = query().first()
val flow = if (shouldFetch(data)) {
val flowContext = currentCoroutineContext()
val loading: Job = coroutineScope {
launch(flowContext) {
query().map { Resource.Loading(it) }
.collect { withContext(flowContext) { emit(it) } }
}
}
try {
val request = fetch()
loading.cancel()
saveFetchResult(request)
query().map { Resource.Success(it) }
} catch (throwable: Throwable) {
loading.cancel()
onFetchFailed(throwable)
query().map { Resource.Error(throwable, it) }
}
} else {
query().map { Resource.Success(it) }
}
emitAll(flow)
}
Let me know if this works out!
Hi I have a rxjava flat map in which I want to call a coroutine usecase onStandUseCase which is an api call
Initially the use case was also rxjava based and it used to return Observable<GenericResponse> and it worked fine
now that I changed the use to be coroutines based it only returns GenericResponse
how can modify the flatmap to work fine with coroutines use case please
subscriptions += view.startFuellingObservable
.onBackpressureLatest()
.doOnNext { view.showLoader(false) }
.flatMap {
if (!hasOpenInopIncidents()) {
//THIS IS WHERE THE ERROR IS IT RETURNS GENERICRESPONSE
onStandUseCase(OnStandUseCase.Params("1", "2", TimestampedAction("1", "2", DateTime.now()))) {
}
} else {
val incidentOpenResponse = GenericResponse(false)
incidentOpenResponse.error = OPEN_INCIDENTS
Observable.just(incidentOpenResponse)
}
}
.subscribe(
{ handleStartFuellingClicked(view, it) },
{ onStartFuellingError(view) }
)
OnStandUseCase.kt
class OnStandUseCase #Inject constructor(
private val orderRepository: OrderRepository,
private val serviceOrderTypeProvider: ServiceOrderTypeProvider
) : UseCaseCoroutine<GenericResponse, OnStandUseCase.Params>() {
override suspend fun run(params: Params) = orderRepository.notifyOnStand(
serviceOrderTypeProvider.apiPathFor(params.serviceType),
params.id,
params.action
)
data class Params(val serviceType: String, val id: String, val action: TimestampedAction)
}
UseCaseCoroutine
abstract class UseCaseCoroutine<out Type, in Params> where Type : Any {
abstract suspend fun run(params: Params): Type
operator fun invoke(params: Params, onResult: (type: Type) -> Unit = {}) {
val job = GlobalScope.async(Dispatchers.IO) { run(params) }
GlobalScope.launch(Dispatchers.Main) { onResult(job.await()) }
}
}
startFuellingObservable is
val startFuellingObservable: Observable<Void>
Here is the image of the error
Any suggestion on how to fix this please
thanks in advance
R
There is the integration library linking RxJava and Kotlin coroutines.
rxSingle can be used to turn a suspend function into a Single. OP wants an Observable, so we can call toObservable() for the conversion.
.flatMap {
if (!hasOpenInopIncidents()) {
rxSingle {
callYourSuspendFunction()
}.toObservable()
} else {
val incidentOpenResponse = GenericResponse(false)
incidentOpenResponse.error = OPEN_INCIDENTS
Observable.just(incidentOpenResponse)
}
}
Note that the Observables in both branches contain just one element. We can make this fact more obvious by using Observable#concatMapSingle.
.concatMapSingle {
if (!hasOpenInopIncidents()) {
rxSingle { callYourSuspendFunction() }
} else {
val incidentOpenResponse = GenericResponse(false)
incidentOpenResponse.error = OPEN_INCIDENTS
Single.just(incidentOpenResponse)
}
}
I am trying to unit test the kotlin coroutines. My project is following MVP pattern where the coroutines are used in the presenter like this:
fun authenticateWithUserAndPassword(usernameOrEmail: String, password: String) {
launchUI(strategy) {
view.showLoading()
try {
val token = retryIO("login") {
when {
settings.isLdapAuthenticationEnabled() ->
client.loginWithLdap(usernameOrEmail, password)
usernameOrEmail.isEmail() ->
client.loginWithEmail(usernameOrEmail, password)
else ->
client.login(usernameOrEmail, password)
}
}
val myself = retryIO("me()") { client.me() }
myself.username?.let { username ->
val user = User(
id = myself.id,
roles = myself.roles,
status = myself.status,
name = myself.name,
emails = myself.emails?.map { Email(it.address ?: "", it.verified) },
username = username,
utcOffset = myself.utcOffset
)
localRepository.saveCurrentUser(currentServer, user)
saveCurrentServer.save(currentServer)
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, username)
saveAccount(username)
saveToken(token)
analyticsManager.logLogin(
AuthenticationEvent.AuthenticationWithUserAndPassword,
true
)
view.saveSmartLockCredentials(usernameOrEmail, password)
navigator.toChatList()
}
} catch (exception: RocketChatException) {
when (exception) {
is RocketChatTwoFactorException -> {
navigator.toTwoFA(usernameOrEmail, password)
}
else -> {
analyticsManager.logLogin(
AuthenticationEvent.AuthenticationWithUserAndPassword,
false
)
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
}
} finally {
view.hideLoading()
}
}
}
Here launchUI is defined as
/**
* Launches a coroutine on the UI context.
*
* #param strategy a CancelStrategy for canceling the coroutine job
*/
fun launchUI(strategy: CancelStrategy, block: suspend CoroutineScope.() -> Unit): Job =
MainScope().launch(context = strategy.jobs, block = block)
CancelStrategy class
class CancelStrategy(owner: LifecycleOwner, val jobs: Job) : LifecycleObserver {
init {
owner.lifecycle.addObserver(this)
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
jobs.cancel()
}
}
I am new to coroutine unit testing and tried writing a basic unit test
#Test
fun check_authentication_of_user() = runBlocking {
loginPresenter.authenticateWithUserAndPassword(USERNAME, PASSWORD)
verify(navigator).toChatList()
}
On running the test I am getting the following error.
java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlinx.coroutines.BuildersKt__Builders_commonKt.launch, parameter context
Any help regarding how to unit test the given method will be appreciated.