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
}
Related
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)
}
}
I'm trying to test multiple server responses with Mockk library. Something like I found in this answer for Mockito.
There is my sample UseCase code, which every few seconds repeats call to load the system from a remote server and when the remote system contains more users than local it stops running (onComplete is executed).
override fun execute(localSystem: System, delay: Long): Completable {
return cloudRepository.getSystem(localSystem.id)
.repeatWhen { repeatHandler -> // Repeat every [delay] seconds
repeatHandler.delay(params.delay, TimeUnit.SECONDS)
}
.takeUntil { // Repeat until remote count of users is greater than local count
return#takeUntil it.users.count() > localSystem.users.count()
}
.ignoreElements() // Ignore onNext() calls and wait for onComplete()/onError() call
}
To test this behavior I'm mocking the cloudRepository.getSystem() method with the Mockk library:
#Test
fun testListeningEnds() {
every { getSystem(TEST_SYSTEM_ID) } returnsMany listOf(
Single.just(testSystemGetResponse), // return the same amount of users as local system has
Single.just(testSystemGetResponse), // return the same amount of users as local system has
Single.just( // return the greater amount of users as local system has
testSystemGetResponse.copy(
owners = listOf(
TEST_USER,
TEST_USER.copy(id = UUID.randomUUID().toString())
)
)
)
)
useCase.execute(
localSystem = TEST_SYSTEM,
delay = 3L
)
.test()
.await()
.assertComplete()
}
As you can see I'm using the returnsMany Answer which should return a different value on every call.
The main problem is that returnsMany returns the same first value every time and .takeUntil {} never succeeds what means that onComplete() is never called for this Completable. How to make returnsMany return a different value on each call?
You probably don't understand how exactly .repeatWhen() works. You expect cloudRepository.getSystem(id) being called every time repetition is requested. That is not correct. Repeated subscription is done all the time on the same instance of mocked Single – first Single.just(testSystemGetResponse) in your case.
How to make sure, getSystem() is called every time? Wrap your Single into Single.defer(). It's similar to Single.fromCallable() but there is a difference between the return type of passed lambda. Lambda passed to the .defer() operator must return Rx type (Single in our case).
Final implementation (I have made a few changes to make it compile successfully):
data class User(val id: String)
data class System(val users: List<User>, val id: Long)
class CloudRepository {
fun getSystem(id: Long) = Single.just(System(mutableListOf(), id))
}
class SO63506574(
private val cloudRepository: CloudRepository
) {
fun execute(localSystem: System, delay: Long): Completable {
return Single.defer { cloudRepository.getSystem(localSystem.id) } // <-- defer
.repeatWhen { repeatHandler ->
repeatHandler.delay(delay, TimeUnit.SECONDS)
}
.takeUntil {
return#takeUntil it.users.count() > localSystem.users.count()
}
.ignoreElements()
}
}
And test (succeeds after ~8s):
class SO63506574Test {
#Test
fun testListeningEnds() {
val TEST_USER = User("UUID")
val TEST_SYSTEM = System(mutableListOf(), 10)
val repository = mockk<CloudRepository>()
val useCase = SO63506574(repository)
val testSystemGetResponse = System(mutableListOf(), 10)
every { repository.getSystem(10) } returnsMany listOf(
Single.just(testSystemGetResponse), // return the same amount of users as local system has
Single.just(testSystemGetResponse), // return the same amount of users as local system has
Single.just( // return the greater amount of users as local system has
testSystemGetResponse.copy(
users = listOf(
TEST_USER,
TEST_USER.copy(id = UUID.randomUUID().toString())
)
)
)
)
useCase.execute(
localSystem = TEST_SYSTEM,
delay = 3L
)
.test()
.await()
.assertComplete()
}
}
Using the code below to test exceptions with Flow and MockK
#Test
fun given network error returned from repos, should throw exception() =
testCoroutineScope.runBlockingTest {
// GIVEN
every { postRemoteRepository.getPostFlow() } returns flow<List<PostDTO>> {
emit(throw Exception("Network Exception"))
}
// WHEN
var expected: Exception? = null
useCase.getPostFlow()
.catch { throwable: Throwable ->
expected = throwable as Exception
println("⏰ Expected: $expected")
}
.launchIn(testCoroutineScope)
// THEN
println("⏰ TEST THEN")
Truth.assertThat(expected).isNotNull()
Truth.assertThat(expected).isInstanceOf(Exception::class.java)
Truth.assertThat(expected?.message).isEqualTo("Network Exception")
}
And prints
⏰ TEST THEN
⏰ Expected: java.lang.Exception: Network Exception
And the test fails with
expected not to be: null
Method i test is
fun getPostFlow(): Flow<List<Post>> {
return postRemoteRepository.getPostFlow()
// 🔥 This method is just to show flowOn below changes current thread
.map {
// Runs in IO Thread DefaultDispatcher-worker-2
println("⏰ PostsUseCase map() FIRST thread: ${Thread.currentThread().name}")
it
}
.flowOn(Dispatchers.IO)
.map {
// Runs in Default Thread DefaultDispatcher-worker-1
println("⏰ PostsUseCase map() thread: ${Thread.currentThread().name}")
mapper.map(it)
}
// This is a upstream operator, does not leak downstream
.flowOn(Dispatchers.Default)
}
This is not a completely practical function but just to check how Dispatchers work with Flow and tests.
At the time of writing the question i commented out .flowOn(Dispatchers.IO), the one on top
the test passed. Also changing Dispatchers.IO to Dispatchers.Default and caused test to pass. I assume it's due to using different threads.
1- Is there a function or way to set all flowOn methods to same thread without modifying the code?
I tried testing success scenario this time with
#Test
fun `given data returned from api, should have data`() = testCoroutineScope.runBlockingTest {
// GIVEN
coEvery { postRemoteRepository.getPostFlow() } returns flow { emit(postDTOs) }
every { dtoToPostMapper.map(postDTOs) } returns postList
val actual = postList
// WHEN
val expected = mutableListOf<Post>()
// useCase.getPostFlow().collect {postList->
// println("⏰ Collect: ${postList.size}")
//
// expected.addAll(postList)
// }
val job = useCase.getPostFlow()
.onEach { postList ->
println("⏰ Collect: ${postList.size}")
expected.addAll(postList)
}
.launchIn(testCoroutineScope)
job.cancel()
// THEN
println("⏰ TEST THEN")
Truth.assertThat(expected).containsExactlyElementsIn(actual)
}
When i tried to snippet that is commented out, test fails with
java.lang.IllegalStateException: This job has not completed yet
It seems that test could pass if job has been canceled, the one with launchIn that returns job passes.
2- Why with collect job is not canceled, and it only happens when the first flowOn uses Dispatchers.IO?
I am writing unit tests and I have this particular case when I change observable value before executing a suspend function and then right after. I would like to write a unit test to check if the value was changes twice correctly.
Here is the method:
fun doSomething() {
viewModelScope.launch(ioDispatcher) {
try {
viewModelScope.launch(Dispatchers.Main) {
commandLiveData.value = Pair(CANCEL_TIMER, null)
}
isProgress.set(true) //need to test it was set to true in one test
checkoutInteractor.doSomethingElse().run {
handleResult(this)
}
isProgress.set(false) //need to test it was set to false here in another test
} catch (ex: java.lang.Exception) {
handleHttpError(ex)
isProgress.set(false)
}
}
}
When writing a test I am calling doSomething() but I am unsure how to detect that the value was set to true before the checkoutInteractor.doSomethingElse call and set to false after.
Here is the test I have
#Test
fun `test doSomething enables progress`() {
runBlockingTest {
doReturn(Response()).`when`(checkoutInteractor). checkoutInteractor.doSomethingElse()
viewModel.doSomething()
assertTrue(viewModel.isProgress.get()) //fails obviously as the value was already set to false after the `doSomethingElse` was executed.
}
}
BTW, isProgress is an ObservableBoolean
You would need to either mock or spy on isProgress and checkoutInteractor fields to record and verify the execution of their methods.
Pass a mock or spy for isProcess and checkoutInteractor into your class.
Execute doSomething()
Verify inOrder the set() and doSomethingElse() functions
Example:
#Test
fun `test doSomething enables progress`() {
runBlockingTest {
val checkoutInteractor = Mockito.spy(CheckoutInteractor())
val isProcess = Mockito.spy(ObservableBoolean(true))
val viewModel = ViewModel(isProcess, checkoutInteractor)
viewModel.doSomething()
val inOrder = inOrder(isProcess, checkoutInteractor)
inOrder.verify(isProcess).set(true)
inOrder.verify(checkoutInteractor).doSomethingElse()
inOrder.verify(isProcess).set(false)
}
}
The method I want to test contains of two calls to the retrofit service:
internal fun poll(): Completable {
return presenceService.askForFrequency(true).toObservable()
.flatMap { it -> Observable.interval(it.frequency, TimeUnit.SECONDS, Schedulers.io()) }
.flatMapCompletable { _ -> presenceService.sendHeartbeat() }
.subscribeOn(Schedulers.io())
.retry()
}
The presenceService is injected in the class, so I provide the mocked one for the test:
val frequency = PublishSubject.create<Presence>()
val heartbeat = PublishSubject.create<Unit>()
val mockPresenceService = mock<PresenceService> {
on { askForFrequency(any()) } doReturn frequency
on { sendHeartbeat() } doReturn heartbeat
}
The test, that checks that askForFrequency method is called works correctly, but test that checks that the polling request is sent never works:
#Test
fun presenceService_sentHeartbeat() {
RxJavaPlugins.setIoSchedulerHandler { scheduler }
frequency.onNext(Presence(1)) //polls with 1s interval
heartbeat.onNext(Unit)
presenceMaintainer.onActivityResumed(any())
scheduler.advanceTimeBy(2, TimeUnit.SECONDS)
verify(mockPresenceService).askForFrequency(true) //works correctly
verify(mockPresenceService).sendHeartbeat() //never works
}
The logs from the unit test run are:
Wanted but not invoked:
presenceService.sendHeartbeat();
However, there was exactly 1 interaction with this mock:
presenceService.askForFrequency(true);
The question is: how to test that the second method (sendHeartbeat) is also called (possibly several times)?
Meanwhile I found out that the problem lies in the second flatmap, because the test for this method works correctly (verifies that method was called 60 times):
internal fun pollTest(): Observable<Presence> {
return Observable.interval(1, TimeUnit.SECONDS, Schedulers.io())
.subscribeOn(Schedulers.io())
.flatMap { it -> presenceService.askForFrequency(true).toObservable() }
}
#Test
fun presenceService_sentHeartbeat() {
frequency.onNext(Presence(1))
val result = arrayListOf<Unit>()
presenceMaintainer.pollTest().subscribe({ t -> result.add(Unit) })
Thread.sleep(60*1000)
println(result.size)
verify(mockPresenceService, Times(60)).askForFrequency(true)
}
But when I change the order of the calls to askForFrequency -> map to interval -> map each tick to poll call, test stops working and mock is called only once.
By default, Observable.interval() runs on the computation scheduler, and not the io scheduler. That means, that the 2 second wait will be run in real time, so your test will finish 2 seconds before the call to sendHeartBeat().