Search code examples
androidunit-testingrx-javarx-java2mockk

Testing RxJava repeatWhen with Mockk returnsMany


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?


Solution

  • 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()
        }
    }