I'm trying to unit test my viewmodel code, which does a server polling to return data as soon as its Status == ELIGIBLE
My problem is, it always assert when it's still loading (repeating), and not waiting for the onSuccess to be called to assert the correct status.
I've put some logs to track what's happening:
doOnSubscribe called
repeatWhen called
doOnNext called
takeUntil called
doOnNext called
takeUntil called
As you can see, repeatWhen and takeUntil are called twice (which is expected), but after that, no onSuccess called.
And eventually the test fails with this message
Caused by: java.lang.AssertionError: expected:<SUCCESS> but was:<LOADING>
If I removed the failing line, the next assertion would fail too with message:
java.lang.AssertionError:
Expected :ELIGIBLE
Actual :null
Which mean the onSuccess method is not yet reached, and is still loading.
I also don't prefer using Schedulers.trampoline() .. it works, but it waits for 5 secs synchronously. I prefer to use TestScheduler.advanceByTime() instead.
Here's the client code:
fun startStatusPolling() {
val pollingSingle = shiftPayService.obtainCardStatus()
.repeatWhen {
println("repeatWhen called")
//POLLING_INTERVAL_SECONDS = 5
it.delay(POLLING_INTERVAL_SECONDS, TimeUnit.SECONDS)
}
.takeUntil { item ->
println("takeUntil called")
item.cardStatus != Status.PENDING
}.doOnNext {
println("doOnNext called")
}
.lastElement()
.toSingle()
subscribe(pollingSingle, pollingStatusLiveData)
}
And my test class:
#RunWith(HomebaseRobolectricTestRunner::class)
#LooperMode(LooperMode.Mode.PAUSED)
class CardViewModelTest {
lateinit var viewModel: CardViewModel
var testScheduler = TestScheduler()
#Before
fun setup() {
RxJavaPlugins.setComputationSchedulerHandler { testScheduler }
RxJavaPlugins.setIoSchedulerHandler { testScheduler }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { testScheduler }
val cardStatusPending: CardStatus = mockk(relaxed = true) {
every { status } returns Status.PENDING
}
val cardStatusEligible: CardStatus = mockk(relaxed = true) {
every { status } returns Status.ELIGIBLE
}
val cardService: CardService = spyk {
every { obtainCardStatus() } returnsMany listOf(
Single.just(cardStatusPending),
Single.just(cardStatusEligible)
)
}
viewModel = CardViewModel(cardService)
}
#Test
fun testCardStatusPolling() {
viewModel.startStatusPolling()
shadowOf(Looper.getMainLooper()).idle()
testScheduler.advanceTimeBy(5, TimeUnit.SECONDS)
//after 5 sec delay, single is resubscibed, returning the second single cardStatusEligible
assertEquals(Result.Status.SUCCESS, viewModel.pollingStatusLiveData.value?.status)
assertEquals(EligiblityStatus.ELIGIBLE, viewModel.pollingStatusLiveData.value?.data?.eligibilityStatus)
}
}
Related
example 1]
#Test
fun `validate live data`() = runBlocking {
`when`(repository.GetList()).thenReturn(list)
val vm = TestViewModel(testUseCase, userManager)
idHashMap.keys.forEach {
vm.getList(it)
vm.listLivedata.observeForeEver {
assertEquals(
(vm.listLivedata.getOrAwaitValue() as Result.Success<List>)?.data?.listData!!::class.java,
idHashMap[it]
)
assertTrue(
vm.listLiveData.getOrAwaitValue() is Result.Success,
"error"
)
}
}
}
example 2]
#Test
fun `validate live data`() = runBlocking {
`when`(repository.GetList()).thenReturn(list)
val vm = TestViewModel(testUseCase, userManager)
idHashMap.keys.forEach {
vm.getList(it)
vm.listLivedata.observeForeEver {
assertTrue(
vm.listLiveData.getOrAwaitValue() is Result.Success,
"error"
)
}
}
}
1st example always fails the test due to asserEquals() but 2nd always passes a test(when I remove assertEqual()); I wonder what is the reason? Is calling operation that update livedata inside loop, somehow causing livedata issue?
I have Unit test function RxJava with timeout but it doesn't subscribe for unit test.
Function on viewModel
fun loadData() {
loadDataUseCase.loadData(true)
.subscribeOn(Schedulers.io())
.timeout(30L, TimeUnit.SECONDS, schedulers)
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe {
onShowLoading.value = true
onShowError.value = false
onShowContent.value = false
}.subscribe(
{
onConnected.value = true
onShowContent.value = true
onShowError.value = false
onShowLoading.value = false
},
{
onShowError.value = true
onShowLoading.value = false
onShowContent.value = false
}
)
.addTo(compositeDisposable)
}
Function on unit test
#Test
fun `Load data is success`() {
// given
whenever(loadDataUseCase.loadData(true)).thenReturn(Observable.just(true))
// when
viewModel.loadData()
// then
viewModel.onShowError().test().assertValue(false).awaitNextValue().assertValue(false)
}
I try to debug this function but it doesn't invoke subscribe
I am a bit confused, what is onShowError() does it return a LiveData?
If I run the same code the test doesn't even finish (well I use only io dispatchers and postValue), for you it might be finished before the subscription even happens:
Since you rely on Schedulers.io() it is possible that your whole Subscription is finished before you get to even test your LiveData.
An other option is that your LiveData already has a false value: .assertValue(false). then the next .doOnSubscribe setting already triggers .awaitNextValue() and your whole test finishes, before the subscription can even be called.
Your tests should be fixed and not dependent on timing. Or if it is unavoidable then you have to synchronize your test somehow, an example of this is here:
#Timeout(1, unit = TimeUnit.SECONDS)
#Test
fun `Load data is success`() {
// given
whenever(loadDataUseCase.loadData(true)).thenReturn(Observable.just(true)
val testObserver = liveData.test()
testObserver.assertNoValue() // assert in correct state before actions
// when
loadData(liveData, mock)
//then
testObserver.awaitValueHistorySize(2)
.assertValueHistory(false, false)
}
fun <T> TestObserver<T>.awaitValueHistorySize(count: Int, delay: Long = 10): TestObserver<T> {
awaitValue()
while (valueHistory().size < count) {
// we need to recheck and don't block, the value we are trying to wait might already arrived between the while condition and the awaitNextValue in the next line, so we use some delay instead.
awaitNextValue(delay, TimeUnit.MILLISECONDS)
}
return this
}
When the response came with empty body, Retrofit BodyObservable calls onNext() with null value. In RxJava2 that leads to NPE, but that is not an issue, and there's a recommended way to deal with it using Completable or Optional converter (https://github.com/square/retrofit/issues/2242).
The problem I have encountered that in some scenarios onError() handler is not triggered at all, so that observable silently fails when getting NPE (making it harder to spot and fix). I've managed to isolate this case:
data class Item(val name: String)
class Retrofit2WithRxJava2TestCase {
#Rule
#JvmField
public val server: MockWebServer = MockWebServer()
private var disposable: Disposable? = null
interface Service {
#GET("/{path}")
fun getItem(
#Path(value = "path", encoded = true) path: String?
): Observable<Item>
#DELETE("/{path}")
fun deleteItem(
#Path(value = "path", encoded = true) path: String?
): Observable<Item>
}
#Test
#Throws(Exception::class)
fun test() {
val retrofit = Retrofit.Builder()
.baseUrl(server.url("/"))
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
val example = retrofit.create(Service::class.java)
server.enqueue(
okhttp3.mockwebserver.MockResponse()
.addHeader("Content-Type", "text/json")
.setBody("""{"name": "Name"}""")
)
server.enqueue(
okhttp3.mockwebserver.MockResponse()
.setResponseCode(204)
)
val observable: Observable<Item> = example.getItem("hello")
val observable2: Observable<Item> = example.deleteItem("hello")
val countDownLatch = CountDownLatch(1)
disposable = observable
.flatMap {
observable2
//.doOnError { // <- This one would be triggered
// println("doOnError triggered")
//}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally { // <- Never reaches here
countDownLatch.countDown()
println("doFinally")
}
.subscribe({ next ->
println("onNext")
}, { error -> // <- Never reaches here
println("onError $error")
})
val result = countDownLatch.await(7, TimeUnit.SECONDS)
Assert.assertTrue(result)
}
}
So, the question is why the NPE is not propagated to onError() in this case, when observable2 inside .flatMap{} gets empty body, and hence triggers onNext() with null value?
Thank you for all suggestions.
EDIT:
Simplifying test observable to make sure unit test thread won't exit before observable completion:
observable.flatMap {
//Observable.error<Throwable>(NullPointerException()) //<- Would reach onError()
observable2
}
.subscribeOn(Schedulers.trampoline())
.observeOn(Schedulers.trampoline())
.doFinally { // <- Never reaches here
println("doFinally")
}
.blockingSubscribe({ next ->
println("onNext")
}, { error -> // <- Never reaches here
println("onError $error")
})
I have a retrofit service
interface Service {
#PUT("path")
suspend fun dostuff(#Body body: String)
}
It is used in android view model.
class VM : ViewModel(private val service: Service){
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
val state = MutableLiveData<String()
init {
uiScope.launch {
service.doStuff()
state.value = "lol"
}
}
override fun onCleared(){
viewModelJob.cancel()
}
}
I would like to write a test for the cancelling of the view model. This will be done mocking service and delaying so that the co routine does not complete. Whilst blocking, we invoke onCleared to cancel the co routine. This should prevent state getting set...
#Test
fun `on cleared - cancels request`() = runBlocking {
//given
`when`(service.doStuff()).thenAnswer { launch { delay(1000) } }
val vm = ViewModel(service)
// when
vm.cleared()
//then
assertThat(vm.state, nullValue())
}
However it seems that vm.state always gets set??? What is the best way to test when clearing a scope that a co routine gets cancelled?
The problem here is in thenAnswer { launch { delay(1000) } }, which effectively makes your doStuff method look like that:
suspend fun doStuff() {
launch { delay(1000) }
}
As you can see, this function does not actually suspend, it launches a coroutine and returns immediately. What would actually work here is thenAnswer { delay(1000) }, which does not work, because there is no suspend version of thenAnswer in Mockito (as far as I know at least).
I would recommend to switch to Mokk mocking library, which supports kotlin natively. Then you can write coEvery { doStuff() } coAnswers { delay(1000) } and it will make your test pass (after fixing all the syntax errors ofc).
I have a list of completables that by default I run them one after one with concat/andThen operators.
Sometimes I want some part of the completables to run in parallel and after everything complete continue to the next completable in the list.
I tried to achieve that with this code:
var completable =
getAsyncCompletables()?.let {
it
} ?: run {
completables.removeAt(0).getCompletable()
}
while (completables.isNotEmpty()) {
val nextCompletable = getAsyncCompletables()?.let {
it
} ?: run {
completables.removeAt(0).getCompletable()
}
completable = nextCompletable.startWith(completable)
}
completable
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe()
I use this code to detect the async completables:
private fun getAsyncCompletables(): Completable? {
if (completables.size < 2 || !completables[1].async) {
return null
}
var completable = completables.removeAt(0).getCompletable()
while (completables.isNotEmpty() && completables[0].async) {
completable = completable.mergeWith(completables.removeAt(0).getCompletable())
}
return completable
}
All works fine, except one thing, the last completable not triggered althought I used "startWith".
I also tried "concatWith" and "andThen",but same result.
It is a bit difficult to answer without seeing more of your code, specifically what async does and what the data structure is for completables. However, the answer you are looking for is most likely similar regardless of these values. You will probably want to use Completable.merge(...) or Completable.mergeArray(...).
As per the documentation:
/**
* Returns a Completable instance that subscribes to all sources at once and
* completes only when all source Completables complete or one of them emits an error.
* ...
*/
In order to achieve the parallel execution, you will need to call subscribeOn each of your Completables in the list/array/set with a new thread. This can be done with Schedulers.newThread() or from a shared pool like Schedulers.io().
I ran a test just to be sure. Here is the code.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
val completeOne = Completable.fromAction {
Timber.d("Completable #1 running on ${Thread.currentThread().name}")
}
val completeTwo = Completable.fromAction {
Timber.d("Completable #2 running on ${Thread.currentThread().name}")
}
val completeThree = Completable.fromAction {
Timber.d("Completable #3 running on ${Thread.currentThread().name}")
}
val completables = listOf(completeOne, completeTwo, completeThree).map { CompletableWrapper(it) }
val asyncCompletables = completables
.asSequence()
.filter { it.async }
.map { it.getCompletable().subscribeOn(Schedulers.io()) }
.toList()
Completable.merge(asyncCompletables)
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
Timber.i("Completed all completables")
}, Timber::e)
}
class CompletableWrapper(
private val completable: Completable,
val async: Boolean = true
) {
fun getCompletable() = completable
}
And here is the output.
D/MainActivity$onCreate$completeThree: Completable #3 running on RxCachedThreadScheduler-3
D/MainActivity$onCreate$completeTwo: Completable #2 running on RxCachedThreadScheduler-2
D/MainActivity$onCreate$completeOne: Completable #1 running on RxCachedThreadScheduler-1
I/MainActivity$onCreate: Completed all completables
As you can see, it runs each completable on a new thread from the pool and only calls completed all after each completable has finished.
See here for the documentation on Completable.merge/mergeArray.