I am writing a testing code using Mockito.
I am using RxJava and Retrofit to get the resource from the server.
mockRestService.get(id) method returns Observable.
First, the request call returns an item with the status of "not ready".
So I should use the Rx operator "repeatWhen()".
After some retry, the server sends an item with the status of "complete".
Below is my Presenter code.
val getItem = restService.getItem(id)
.repeatWhen { it.delay(1000, TimeUnit.MILLISECONDS) }
.takeUntil { it.status == "complete" }
And below is my testing code.
To mimic server behaviour, I wrote below the testing code.
#Test
fun printJobTest_one_job_normal_case() {
val notReadyItem = Item(status = "not ready")
val completeItem = Item(status = "complete")
Mockito.`when`(mockRestService.getItem(id))
.thenReturn(Observable.just(notReadyItem)) // First 2 response is "not ready" status
.thenReturn(Observable.just(notReadyItem))
.thenReturn(Observable.just(completeItem)) // Third response is "complete" status
// verify
}
To mimic the server behaviour, I used chained "thenReturn()" method.
But only always the item which's status is "not ready" occurs.
I... found the solution.
It's easy...
Just below code works fine.
#Test
fun printJobTest_one_job_normal_case() {
val notReadyItem = Item(status = "not ready")
val completeItem = Item(status = "complete")
Mockito.`when`(mockRestService.getItem(id))
.thenReturn(Observable.just(notReadyItem, notReadyItem, completeItem))
// verify
}
I just removed the chained method "thenReturn", and moved the variables to the parameter of Observable.
Related
I see that Flow has a retry mechanism, but my use case is somehow different from what I see in the doc, I have a fragment containing a list that fills from API when opening this fragment, but the API calls may be failed and throw an exception for any reason, in this case, I want to show a button that calls the API again on clicking, as follows:
Repository
fun getData(): Flow<Result<T>> = service.getData()
ViewModel
val data: Flow<Result<T>> = repo.getData()
Fragment
viewModel.data.collect{ result ->
if(result is Error){
showRetryButton() // Show the retry button on failed API
}
....
}
retryButton.setOnClickListener{
// do something to retry the API call
}
Can Flow retry help me here? if not, what do you think is the best way to call the failed API again?
Thanks in advance :)
I don't think retry can help in this case. Basically to fire an API request repo.getData() should be called, so just call it and collect returned Flow:
// Fragment:
private var job: Job? = null // save here the job of collecting data
private fun fetchData() {
job?.cancel() // cancel previous job
job = lifecycleScope.launch {
viewModel.getData().collect {
if(result is Error) {
showRetryButton() // Show the retry button on failed API
}
//...
}
}
}
retryButton.setOnClickListener{
fetchData()
}
// ViewModel:
fun getData() = repo.getData()
I have a situation where I want the livedata to be observed only once in the app. The problem is that I am working on the authentication for an app using some Node.js backend.
As I am sending the values to receive the response from the backend it's working fine till now. I observe that response and based on that I make changes to my fragment ( that is if the response received is true then move to next fragment, otherwise if it is false show a toast message ).
Now the problem is that :
Case 1: I opened the app, entered the right credentials and pressed the button, received true response from the server and goes to the next fragment.
Case 2: I opened the app, but entered the wrong credentials, I received a false from server and based on that the Toast is shown.
Case 3 (The issue): I opened the app, entered the wrong credentials and then without closing the fragment screen entered the right credentials by editing them, the app crashes and at the same time I receive multiple responses from the server via LiveData.
My observation: Looking more into that I found that the LiveData is attached to the fragment/activity and therefore it shows the last state. So as in case 3 the the last state was receiving the false value from backend it was used again and we were shown the error instead of going to the next screen.
Can anyone guide me how to solve this. Thanks
Some code that might be needed:
binding.btnContinue.setOnClickListener {
val number = binding.etMobileNumber.text.toString().toLong()
Timber.d("Number: $number")
authRiderViewModel.authDriver(number)
checkNumber()
}
Function which checks the number :
private fun checkNumber() {
authRiderViewModel.response.observe(viewLifecycleOwner, Observer {
Timber.d("Response: $it")
if (it!!.success == true) {
val action = LoginFragmentDirections.actionLoginFragmentToOtpFragment()
findNavController().navigate(action)
Timber.d("${it.message}")
} else {
Toast.makeText(requireContext(), "Number not registered", Toast.LENGTH_SHORT).show()
binding.etMobileNumber.setText("")
}
})
}
ViewModel code:
private val _response = MutableLiveData<AuthResponse>()
val response: LiveData<AuthResponse>
get() = _response
fun authDriver(number: Long) = viewModelScope.launch {
Timber.d("Number: $number")
myRepo.authDriver(number).let {
_response.postValue(it)
}
}
P.S I have tried using something called SingleLiveEvent but it doesn't seem to work.
I would create a separate class that tracks the UI state you need and update it when the state is consumed. Something like the following. I don't really know what the parameter is for authDriver, so this is a more generic example.
sealed interface AuthState {
object NotYetRequested: AuthState
object AwaitingResponse: AuthState
class ResponseReceived(val response: AuthResponse): AuthState {
var isHandled = false
private set
fun markHandled() {
isHandled = true
}
}
}
// In ViewModel:
private val _authState = MutableLiveData<AuthState>().also {
it.value = AuthState.NotYetRequested
}
val authState: LiveData<AuthState> get() = _authState
fun requestAuthentication() = viewModelScope.launch {
_authState.value = AuthState.AwaitingResponse
val response = myRepo.authenticate()
_authState.value = AuthState.ResponseReceived(response)
}
// In Fragment:
viewModel.authState.observe(viewLifecycleOwner) { authState ->
when (authState) {
AuthState.NotYetRequested -> ShowUiRequestingAuthentication()
AuthStateAwaitingResponse -> ShowIndeterminateProgressUi()
is AuthStateResponseReceived -> when {
authState.isHandled -> {} // do nothing? depends on your setup, might need to navigate to next screen if handled response is successful
authState.response.isSuccessful -> {
goToNextScreen()
authState.markHandled()
}
else -> {
showErrorToast()
ShowUiRequestingAuthentication()
authState.markHandled()
}
}
}
}
As said in title, does LiveDataScope block of code runs only one time after emit()?
Is it possible to make my LiveDataScope block of code run more than one time, because i need to make request to server, and if it fails i would like to call same code to try again.
Sample of code:
fun refreshLiveDataResource() = liveData(Dispatchers.Main){
val retriveRoutes = remoteDataSourceKt.getRoutes()
if(retriveRoutes.data != null){
routeList = retriveRoutes.data
}
emit(retriveRoutes)
}
when i call this function, after emit() if i call function again, never gets inside.
You should create a LiveData once and update its value whenever you need it instead of creating a new one everytime you want to retry. I would use a Flow instead, to update the LiveData, with a retry operator in case there is some error, like this:
val routesLiveData =
remoteDataSourceKt.getRoutes()
.onEach { retriveRoutes ->
if(retriveRoutes.data != null){
routeList = retriveRoutes.data
}
}
.retry(3) { e -> // retries up to 3 times; no argument means retrying forever
(e is IOException) // retry on any IOException but also introduce delay if retrying
.also {
if (it)
delay(1000)
}
}
.asLiveData()
You'll have to:
Make getRoutes() method return a Flow.
Make sure the Flow works on Dispatchers.IO.
Observe myLiveData from your UI.
I'm testing a view model which has the following definition:
class PostViewModel(private val postApi: PostApi): ViewModel() {
private val _post: PublishSubject<Post> = PublishSubject.create()
val postAuthor: Observable<String> = _post.map { it.author }
fun refresh(): Completable {
return postApi.getPost() // returns Single<Post>
.doOnSuccess {
_post.onNext(it)
}
.ignoreElement()
}
}
}
My fragment then displays the post author by subscribing to viewModel.postAuthor in its onActivityCreated and calling and subscribing to refresh() whenever the user wants an updated post and everything is fine and dandy.
The issue I'm running into is trying to verify this behaviour in a unit test: specifically, I am unable to get postAuthor to emit an event in my testing environment.
My test is defined as follows:
#Test
fun `When view model is successfully refreshed, display postAuthor`() {
val post = Post(...)
whenever(mockPostApi.getPost().thenReturn(Single.just(post))
viewModel.refresh()
.andThen(viewModel.postAuthor)
.test()
.assertValue { it == "George Orwell" }
}
The test fails due to no values or errors being emitted, even though I can verify through the debugger that the mock does in-fact return the Post as expected. Is there something obvious that I'm missing, or am I completely wrong in my testing approach?
viewModel.postAuthor is a hot-observable. It emits value when you call _post.onNext(it).
Unlike a cold-observable, the late subscribers cannot receive the values that got emitted before they subscribe.
So in your case I think the viewModel.postAuthor is subscribed after you call viewModel.refresh(), so it cannot receive the value.
The observable could be emitting on a different thread so that's why it's empty when the test is checking the values/errors.
You could try forcing your observable to emit on the same thread. Depending on which scheduler you're using, it'd be something like:
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
I am testing Kotlin coroutines in my Android app and I am trying to do the following unit test
#Test fun `When getVenues success calls explore venues net controller and forwards result to listener`() =
runBlocking {
val near = "Barcelona"
val result = buildMockVenues()
val producerJob = produce<List<VenueModel>>(coroutineContext) { result.value }
whenever(venuesRepository.getVenues(eq(near))) doReturn producerJob // produce corooutine called inside interactor.getVenues(..)
interactor.getVenues(near, success, error) // call to real method
verify(venuesRepository).getVenues(eq(near))
verify(success).invoke(argThat {
value == result.value
})
}
The interactor method is as follows
fun getVenues(near: String, success: Callback<GetVenuesResult>,
error: Callback<GetVenuesResult>) =
postExecute {
repository.getVenues(near).consumeEach { venues ->
if (venues.isEmpty()) {
error(GetVenuesResult(venues, Throwable("No venues where found")))
} else {
success(GetVenuesResult(venues))
}
}
}
postExecute{..} is a method on a BaseInteractor that executes the function in the ui thread through a custom Executor that uses the launch(UI) coroutine from kotlin android coroutines library
fun <T> postExecute(uiFun: suspend () -> T) =
executor.ui(uiFun)
Then the repository.getVenues(..) function is also a coroutine that returns the ProducerJob using produce(CommonPool) {}
The problem is that it seams that success callback in the interactor function doesn't seem to be executed as per the
verify(success).invoke(argThat {
value == result.value
})
However, I do see while debugging that the execution in the interactor function reaches to the if (venues.isEmpty()) line inside the consumeEach but then from there exits and continues with the test, obviously failing on the verify for the success callback.
I am a bit new on coroutines so any help would be appreciated.
I figured this one out. I saw that the problem was just with this producing coroutine and not with the others tests that are also using coroutines and working just fine. I noticed that I actually missed the send on the mocked ProducingJob in order to have it actually produce a value, in this case the list of mocks. I just added that changing the mock of the producing job to
val producerJob = produce { send(result.value) }