My test case to test the viewmodel looks like this :
#Before
fun setUp() {
loginActivityViewModel = LoginActivityViewModel(loginRepository)
.apply { users.observeForever(userObserver) }
}
#Test
fun `check user response when get successful response from server`() {
testCoroutineRule.runBlockingTest {
//Given
whenever(loginRepository.getLoginResponse(loginRequest)).then(Answer { loginResponse })
//When
loginActivityViewModel.loginResponse(loginRequest)
//Then
verify(userObserver).onChanged(Resource.loading(data = null))
verify(userObserver).onChanged(Resource.success(data = loginResponse))
}
}
#Test
fun `check user response when get unsuccessful response from server`() {
testCoroutineRule.runBlockingTest {
//Given
whenever(loginRepository.getLoginResponse(loginRequest)).thenThrow(Error("Some error"))
//When
loginActivityViewModel.loginResponse(loginRequest)
//Then
verify(userObserver).onChanged(Resource.loading(data = null))
verify(userObserver).onChanged(Resource.error(message = "Some error"))
}
}
Inside this first test case run successfully but when it run 2nd one giving this error:
Wanted but not invoked: userObserver.onChanged(
Resource(status=ERROR, data=null, message=Some error) );
-> at com.android.loginapp.viewmodel.LoginActivityViewModelTest$check user response when get unsuccessful response from
server$1.invokeSuspend(LoginActivityViewModelTest.kt:83)
However, there was exactly 1 interaction with this mock:
userObserver.onChanged(
Resource(status=LOADING, data=null, message=null) );
-> at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)
My viewModel network calling method look like this:
fun loginResponse(loginRequest: LoginRequest) {
viewModelScope.launch {
users.postValue(Resource.loading(null))
try {
val usersFromApi = loginRepository.getLoginResponse(loginRequest)
users.postValue(Resource.success(usersFromApi))
} catch (e: Exception) {
users.postValue(Resource.error(e.message.toString()))
}
}
}
Not sure why it's giving this error.
I need use .thenThrow(RuntimeException("test error")) then only it will pass.
Related
I've written one function with Flow collector which is as shown below,
private fun callSocket(
eventEmmit: String,
eventOn: String,
request: JSONObject
): Flow<SocketCallback<JSONObject>> =
flow {
try {
if (socket.connected()) {
var response = JSONObject()
Log.e("EMIT", JSONObject(Gson().toJson(request)).toString())
socket.on(
eventOn
) { args ->
response = args[0] as JSONObject
Log.e("ON", response.toString())
**this.emit(SocketCallback.OnSuccess(response))**
}.emit(
eventEmmit,
request
)
emit(SocketCallback.OnSuccess(response))
} else {
Log.e("SOCKET_ERROR", "Socket connection failed")
emit(SocketCallback.OnError("Socket connection failed"))
}
} catch (e: SocketException) {
emit(SocketCallback.OnError(e.toString()))
}
}.flowOn(Dispatchers.IO)
But when I write this.emit(SocketCallback.OnSuccess(response))(enclosed in ** in code) in on method it shows me the error "Suspension functions can be called only within coroutine body".
Any solution for this?
Thanks in advance.
You are trying to emit events to flow outside of coroutineScope. socket.on() function probably has signature:
fun on(ev: String, block: (args: String) -> Unit) {
}
in that case, inside lambda block: (args: String) -> Unit) you are outside of scope and you can not invoke suspending functions.
You have only 2 solutions:
Every time new event approach - create new coroutine with coroutine builder launch:
socket.on(
eventOn
) { args ->
response = args[0] as JSONObject
Log.e("ON", response.toString())
launch {
emit(SocketCallback.OnSuccess(response))
}
}.emit(
eventEmmit,
request
)
Use callbackFlow to avoid creation of new coroutine on each event. Please check especially this post.
Here's how I solved it by using the CallBackFlow
private fun callOnSocket(
eventOn: String
): Flow<SocketCallback<JSONObject>> =
callbackFlow<SocketCallback<JSONObject>> {
try {
if (socket.connected()) {
Log.e("ON", "Started")
var response = JSONObject()
socket.on(
eventOn
) {
response = it[0] as JSONObject
Log.e("ON", response.toString())
trySend(SocketCallback.OnSuccess(response))
}
} else {
Log.e("SOCKET_ERROR", "Socket connection failed")
trySend(SocketCallback.OnError("Socket connection failed"))
}
} catch (e: SocketException) {
trySend(SocketCallback.OnError("Socket connection failed"))
}
awaitClose { cancel() }
}.flowOn(Dispatchers.IO)
I made app where user can add server (recycler row) to favorites. It only saves the IP and Port. Than, when user open FavoriteFragment Retrofit makes calls for each server
#GET("v0/server/{ip}/{port}")
suspend fun getServer(
#Path("ip") ip: String,
#Path("port") port: Int
): Server
So in repository I mix the sources and make multiple calls:
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
try {
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
val list: MutableList<Server> = mutableListOf()
getFavoritesServersNotLiveData.forEach { fav ->
val server = soldatApiService.getServer(fav.ip, fav.port)
list.add(server)
}
emit(DataState.Success(list))
} catch (e: Exception) {
emit(DataState.Error(e))
}
}
and then in ViewModel I create LiveData object
fun getFavoriteServers() {
viewModelScope.launch {
repository.getFavoriteServersToRecyclerView()
.onEach { dataState ->
_favoriteServers.value = dataState
}.launchIn(viewModelScope)
}
}
And everything works fine till the Favorite server is not more available in the Lobby and the Retrofit call failure.
My question is: how to skip the failed call in the loop without crashing whole function.
Emit another flow in catch with emitAll if you wish to continue flow like onResumeNext with RxJava
catch { cause ->
emitAll(flow { emit(DataState.Errorcause)})
}
Ok, I found the solution:
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
val list: MutableList<Server> = mutableListOf()
try {
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
val job = CoroutineScope(coroutineContext).launch {
getFavoritesServersNotLiveData.forEach { fav ->
val server = getServer(fav.ip, fav.port)
server.collect { dataState ->
when (dataState) {
is DataState.Loading -> Log.d(TAG, "loading")
is DataState.Error -> Log.d(TAG, dataState.exception.message!!)
is DataState.Success -> {
list.add(dataState.data)
Log.d(TAG, dataState.data.toString())
}
}
}
}
}
job.join()
emit(DataState.Success(list))
} catch (e: Exception) {
emit(DataState.Error(e))
}
}
when using retrofit you can wrap response object with Response<T> (import response from retrofit) so that,
#GET("v0/server/{ip}/{port}")
suspend fun getServer(
#Path("ip") ip: String,
#Path("port") port: Int
): Response<Server>
and then in the Repository you can check if network failed without using try-catch
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
if(getFavoritesServersNotLiveData.isSuccessful) {
val list: MutableList<Server> = mutableListOf()
getFavoritesServersNotLiveData.body().forEach { fav ->
val server = soldatApiService.getServer(fav.ip, fav.port)
// if the above request fails it wont go to the else block
list.add(server)
}
emit(DataState.Success(list))
} else {
val error = getFavoritesServersNotLiveData.errorBody()!!
//do something with error
}
}
I try handling exception using coroutine. I wrote code like this, but didn't work. I can't see any log except for using try-catch. I do not want to use try catch at all function, but want to make clean code handling exception. what should I do for this?
viewmodel
private val handler = CoroutineExceptionHandler { _, exception ->
when (exception) {
is UnknownHostException -> {
showLog("login UnknownHostException : " +exception.message)
}
else -> {
}
}
}
fun login(mobile:String){
viewModelScope.launch(handler) {
try{
var login = apiRepository.login(mobile)
_isLogin.value = login
}catch(e:Exception){
}
}
}
repository
override suspend fun login(mobile: String): LoginResultData {
var result =LoginResultData()
withContext(ioDispatcher){
val request = apiServerModel.login(mobile)
val response = request.await()
result = response
}
return result
}
fun login(mobile:String){
viewModelScope.launch(handler) {
val login = apiRepository.login(mobile)
_isLogin.value = login
}
}
I just want to ask if it is possible to get the response of another observable after encountering an error from the another observable?
for example I am calling a two api Avatar and Attachment using a combineLatest.
val avatar: Observable<ResponseBody> = api().getAvatar()
val attachment: Observable<ResponseBody> = api().getAttachment()
val obs = Observables.combineLatest(avatar, attachment)
.map { it ->
if (it.first is Exception) {
Log.e(TAG, "getAvatar failed")
} else {
updateAvatar()
}
if (it.second is Exception) {
Log.e(TAG, "getAttachment failed")
} else {
updateAttachment()
}
if (it.first !is Exception && it.second !is Exception) {
Log.i(TAG, "success first=${it.first}, second=${it.second}")
updateAll()
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.onErrorReturn { it }
.subscribe()
disposable.add(obs)
I just want to get the avatar response if the attachment error and I want to get the attachment response if the avatar error.
Thanks.
Yes, my friend. You can handle error for each observable that you combine by calling onErrorReturn() method. You can use empty ResponseBody for detecting error. Final code
val avatar: Observable<Optional<ResponseBody>> = api().getAvatar().onErrorReturn{ Optional.empty }
val attachment: Observable<Optional<ResponseBody>> = api().getAttachment().onErrorReturn{ Optional.empty }
val obs = Observables.combineLatest(avatar, attachment) {avatar, attachment ->
if (!avatar.isPresent()) {
//logic
}
if (!attachment.isPresent()) {
//logic
}
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.onErrorReturn { it }
.subscribe()
If you use java 7 or lower in you project, you can write your own Optional
class Optional<T>(val value: T?) {
companion object {
fun <T> empty(): Optional<T> = Optional(null)
}
fun isPresent() = value != null
}
I am trying to use coroutines to handle asynchronous code for my login service. Unfortunately, the implementation of the login service must accept callbacks when it completes. I do not want this login() function to complete until one of these callbacks occurs.
Here is what I have:
fun login(): Outcome = runBlocking {
suspendCoroutine<Outcome> { continuation ->
loginService.login(
onLoginSuccess = {
// do some stuff
continuation.resume(Outcome.SUCCESS)
},
onLoginFailure = {
// handle failure case
continuation.resume(Outcome.FAILURE)
}
)
}
}
My issue is my tests never complete. I think what is happening is that the continuation block itself isn't running. I tried wrapping the call to uut.login() in a runBlocking as well, but it didn't help. Here is my test code (using Spek):
describe("when login") {
val successCaptor: ArgumentCaptor<() -> Unit> = TestHelpers.argumentCaptorForClass()
val failureCaptor: ArgumentCaptor<() -> Unit> = TestHelpers.argumentCaptorForClass()
var result: Outcome? = null
beforeEachTest {
doNothing().whenever(mockLoginService)?.login(capture(successCaptor), capture(failureCaptor))
result = uut?.execute()
}
it("logs in with the login service") {
verify(mockLoginService)?.login(any(), any())
}
describe("and the login succeeds") {
beforeEachTest {
successCaptor.value.invoke()
}
// other tests...
it("returns an outcome of SUCCESS") {
expect(result).to.equal(Outcome.SUCCESS)
}
}
describe("and the login fails") {
beforeEachTest {
failureCaptor.value.invoke()
}
// other tests...
it("returns an outcome of FAILURE") {
expect(result).to.equal(Outcome.FAILURE)
}
}
}
Basically, I'd like to assert that the login() method returned either a SUCCESS or FAILURE outcome based on what occurred.
Any ideas?
Of course, I figured this out right after posting. If interested, here is what I did in the test:
describe("when login") {
val successCaptor: ArgumentCaptor<() -> Unit> = TestHelpers.argumentCaptorForClass()
val failureCaptor: ArgumentCaptor<() -> Unit> = TestHelpers.argumentCaptorForClass()
var result: Outcome? = null
describe("and the login succeeds") {
beforeEachTest {
whenever(mockLoginService?.login(capture(successCaptor), capture(failureCaptor))).thenAnswer {
successCaptor.value.invoke()
}
result = uut?.execute()
}
it("logs in with the login service") {
verify(mockLoginService)?.login(any(), any())
}
it("returns an outcome of SUCCESS") {
expect(result).to.equal(Outcome.SUCCESS)
}
}
describe("and the login fails") {
beforeEachTest {
whenever(mockLoginService?.login(capture(successCaptor), capture(failureCaptor))).thenAnswer {
failureCaptor.value.invoke()
}
result = uut?.execute()
}
it("logs in with the login service") {
verify(mockLoginService)?.login(any(), any())
}
// other tests
it("returns an outcome of FAILURE") {
expect(result).to.equal(Outcome.FAILURE)
}
}
}