I'm not sure if I miss something in MvRx, MvRxTestRule or in RxJava.
It's common that in a ViewModel I want to test a network call and if it triggers another call.
So when I have something like this:
fun fetchResult() {
someRepository.fetchResult()
.compose(schedulerProvider.getSchedulersForSingle())
.execute {
copy(
...
)
}
}
That works in Unit tests:
viewModel?.fetchResult()
viewModel?.someEvent()
withState(viewModel!!) {
assert(it.someOtherRequest == Uninitialized)
}
However if the call is repeating:
fun fetchResult() {
withState { state ->
disposable = Observable.interval(0L, 6L, TimeUnit.SECONDS)
.flatMapSingle {
revolutRepository.fetchResult()
.compose(schedulerProvider.getSchedulersForSingle())
}
.compose(schedulerProvider.getSchedulersForObservable())
.execute {
copy(
...
)
}
}
}
I cannot get past viewModel?.fetchResult() in the test. How can I make it work?
MvRxTestRule() is in the companion object of the tests.
I moved the call with the interval to the repo and mocked that. That solved the problem.
Related
I have This method that calls a Rest API and returns the result as an Observable (Single):
fun resetPassword(email: String): Single<ResetPassword> {
return Single.create { emitter ->
val subscription = mApiInterfacePanda.resetPassword(email)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe({ resetPasswordResponse ->
when(resetPasswordResponse.code()) {
200 -> {
resetPasswordResponse?.body()?.let { resetPassword ->
emitter.onSuccess(resetPassword)
}
}
else -> emitter.onError(Exception("Server Error"))
}
}, { throwable ->
emitter.onError(throwable)
})
mCompositeDisposable.add(subscription)
}
}
Unit Test:
#Test
fun resetPassword_200() {
val response = Response.success(200, sMockResetPasswordResponse)
Mockito.`when`(mApiInterfacePanda.resetPassword(Mockito.anyString()))
.thenReturn(Single.just(response))
mTokenRepository.resetPassword(MOCK_EMAIL)
val observer = mApiInterfacePanda.resetPassword(MOCK_EMAIL)
val testObserver = TestObserver.create<Response<ResetPassword>>()
observer.subscribe(testObserver)
testObserver.assertSubscribed()
testObserver.awaitCount(1)
testObserver.assertComplete()
testObserver.assertResult(response)
}
My Problem is only this line gets covered and the other lines won't run and that has a lot of impact on my total test coverage:
return Single.create { emitter ->
There's more than one thing going on here if I'm not mistaken. Let's take it in parts.
First, your "internal" observer:
mApiInterfacePanda.resetPassword(email)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe({ resetPasswordResponse -> ... })
Is observing on the android main thread and executing on a background thread. To the best of my knowledge, in most cases, the test thread will end before your mApiInterfacePanda .resetPassword has a chance to finish and run. You didn't really post the test setup, so I'm not sure if this is an actual issue, but in any case it's worth mentioning. Here's 2 ways to fix this:
RxJavaPlugins and RxAndroidPlugins
RxJava already provides a way to change the schedulers that are provided. An example is RxAndroidPlugins.setMainThreadSchedulerHandler. Here's how it could help:
#Before
fun setUp() {
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
RxJavaPlugins.setInitIoSchedulerHandler { Schedulers.trampoline() }
}
The above methods make sure that everywhere you use the main thread scheduler and the io scheduler, it'll instead return the trampoline scheduler. This is a scheduler that guarantees that the code is executed in the same thread that was executing previously. In other words, it'll make sure you run it on the unit test main thread.
You will have to undo these:
#After
fun tearDown() {
RxAndroidPlugins.reset()
RxJavaPlugins.reset()
}
You can also change other schedulers.
Inject the schedulers
You can use kotlin's default arguments to help out with injecting schedulers:
fun resetPassword(
email: String,
obsScheduler: Scheduler = AndroidSchedulers.mainThread(),
subScheduler: Scheduler = Schedulers.io()
): Single<ResetPassword> {
return Single.create { emitter ->
val subscription = mApiInterfacePanda.resetPassword(email)
.observeOn(obsScheduler)
.subscribeOn(subScheduler)
.subscribe({ resetPasswordResponse ->
when(resetPasswordResponse.code()) {
200 -> {
resetPasswordResponse?.body()?.let { resetPassword ->
emitter.onSuccess(resetPassword)
}
}
else -> emitter.onError(Exception("Server Error"))
}
}, { throwable ->
emitter.onError(throwable)
})
mCompositeDisposable.add(subscription)
}
}
At test time you can just call it like resetPassword("foo#bar.com", Schedulers.trampoline(), Schedulers.trampoline() and for the application just pass in the email.
The other thing I see here is maybe not related to the problem, but I think it's still good to know. First, you're creating a single, but you don't need to do this.
Single.create is usually used when you don't have reactive code. However, mApiInterfacePanda.resetPassword(email) already returns a reactive component and although I'm not sure, let's just assume it's a single. If not, it should be fairly simple to convert it to something else.
You're also holding on to a disposable, which from what I can tell shouldn't be necessary.
Lastly, you're using retrofit according to your tags so you don't need to make the call return a raw response unless extremely necessary. This is true because retrofit checks the status code for you and will deliver the errors inside onError with an http exception. This is the Rx way of handling the errors.
With all this in mind, I'd rewrite the entire method like this:
fun resetPassword(email: String) = mApiInterfacePanda.resetPassword(email)
(note that resetPassword must not return a raw response, but Single<ResetPassword>
It actually shouldn't need anything else. Retrofit will make sure things end up in either onSuccess or onError. You don't need to subscribe to the result of the api here and handle disposables - let whoever is calling this code handle it.
You may also notice that if this is the case, then the solution for the schedulers is not needed. I guess this is true in this case, just remember some operators operate in some default schedulers and you may need to override them in some cases.
So how would I test the above method?
Personally I'd just check if the method calls the api with the right parameters:
#Test
fun resetPassword() {
mTokenRepository.resetPassword(MOCK_EMAIL)
verify(mApiInterfacePanda).resetPassword(MOCK_EMAIL)
}
I don't think there's much more needed here. There's no more logic I can see in the rewritten method.
I'm trying to create a Flow that needs to emit values from a callback but I can't call the emit function since the SAM is a normal function
Here's the class with the SAM from a library that I can't really modify it the way I need it to be.
class ValueClass {
fun registerListener(listener: Listener) {
...
}
interface Listener {
fun onNewValue(): String
}
}
And here's my take on creating the Flow object
class MyClass(private val valueClass: ValueClass) {
fun listenToValue = flow<String> {
valueClass.registerListener { value ->
emit(value) // Suspension functions can only be called on coroutine body
}
}
}
I guess it would've been simple if I could change the ValueClass but in this case, I can't. I've been wrapping my head around this and trying to look for implementations.
At least from what I know so far, one solution would be to use GlobalScope like this
class MyClass(private val valueClass: ValueClass) {
fun listenToValue = flow<String> {
valueClass.registerListener { value ->
GlobalScope.launch {
emit(value)
}
}
}
}
Now, this works but I don't want to use GlobalScope since I'll be using viewModelScope to tie it to my app's lifecycle.
Is there any way to work around this?
Thanks in advance. Any help would be greatly appreciated!
You can use callbackFlow to create a Flow from the callback. It will look something like:
fun listenToValue(): Flow<String> = callbackFlow {
valueClass.registerListener { value ->
trySend(value)
channel.close() // close channel if no more values are expected
}
awaitClose { /*unregister listener*/ }
}
Or if only one value is expected from the callback, you can use suspendCoroutine or suspendCancellableCoroutine. It this case listenToValue() function must be suspend and later called from a coroutine(e.g. someScope.launch):
suspend fun listenToValue(): String = suspendCoroutine { continuation ->
valueClass.registerListener { value ->
continuation.resumeWith(value)
}
}
I'm creating offline first app as my side project using rxKotlin, MVVM + Clean Architecture and yesterday I decided to get ride off boilerplate subscribeOn and observeOn by using transformers. I quickly realized that apply function of transformers are ignored.
Here is code of my base completable use case (interactor):
abstract class CompletableUseCase(private val transformer: CompletableTransformer) {
abstract fun createCompletable(data: Map<String, Any>? = null) : Completable
fun completable(data: Map<String, Any>? = null) : Completable {
return createCompletable(data).compose(transformer)
}
}
And here is implementation of specific interactor:
class SaveRouteInteractor(
transformer: CompletableTransformer,
private val routeRepository: RouteRepository
) : CompletableUseCase(transformer) {
companion object {
private const val PARAM_ROUTE = "param_route"
}
fun saveRoute(route: Route) : Completable {
val data = HashMap<String, Route>()
data[PARAM_ROUTE] = route
return completable(data)
}
override fun createCompletable(data: Map<String, Any>?): Completable {
val routeEntity = data?.get(PARAM_ROUTE)
routeEntity?.let {
return routeRepository.saveRoute(routeEntity as Route)
} ?: return Completable.error(IllegalArgumentException("Argument #route must be provided."))
}
}
My custom transformer that is passed to the constructor of SaveRouteInteractor:
class IOCompletableTransformer(private val mainThreadScheduler: Scheduler) : CompletableTransformer {
override fun apply(upstream: Completable): CompletableSource {
return upstream.subscribeOn(Schedulers.io()).observeOn(mainThreadScheduler)
}
}
And implementation of RouteRepository method:
override fun saveRoute(route: Route): Completable {
return localRouteSource.saveRoute(route)
.flatMap { localID ->
route.routeId = localID
remoteRouteSource.saveRoute(route)
}
.flatMapCompletable { localRouteSource.updateRouteID(route.routeId, it) }
}
I'm using Room as my local source so after calling save interactor in my ViewModel I'm getting IlligalStateException telling me that I'm not allowed to access database on the main thread.
Maybe I'm missing something but it seems that transform function is ignored. I debugged this method and it is applying subscribeOn and observeOn to the upstream.
Thanks for help in advance,
Pace!
It's hard to tell you where the issue is because the code is partial.
For example here:
return localRouteSource.saveRoute(route)
.flatMap { localID ->
route.routeId = localID
remoteRouteSource.saveRoute(route)
}
.flatMapCompletable { localRouteSource.updateRouteID(route.routeId, it) }
I suppose the localRouteSource.saveRoute() is using the interactor you show us but it is not clear how remoteRouteSource.saveRoute() or localRouteSource.updateRouteID() are implemented.
they also need to be subscribed on the IO thread.
As a rule of thumb you should switch thread when you KNOW that you need it.
In other words, you should use subscribeOn() in places where you know you are doing IO as close as possible to the actual job. ObserveOn instead is to be used when you know you need to obtain those results in the UI thread and that you might get in some other thread.
in your example there's absolutely no need to keep using observeOn(MAIN_THREAD), the only time you do need it (I suppose) is when you want to show the result.
A couple of other things:
This code
override fun createCompletable(data: Map<String, Any>?): Completable {
val routeEntity = data?.get(PARAM_ROUTE)
routeEntity?.let {
return routeRepository.saveRoute(routeEntity as Route)
} ?: return Completable.error(IllegalArgumentException("Argument #route must be provided."))
}
it is evaluated at the time when the method is called rather then when the completable is subscribed.
In other words it break the Rx contract and compute data?.get(PARAM_ROUTE) when you call the method. If it is immutable there's no much difference, but if it can change value during execution it should be wrapped in a Completable.defer { }
Finally, here
.flatMap { localID ->
route.routeId = localID
remoteRouteSource.saveRoute(route)
}
you are modyfing something outside the chain (route.routeId = localID), this is called a side effect.
be careful with those kind of stuff, Rx is build in a way that is safer to be used with immutable objects.
I personally wouldn't mind too much as long as you understand what's going on and when it could create issues.
I am creating a library and I'm using Retrofit with a call-adapter that gives me a Deferred<> value.
In a function in my code I call launch {}, and inside that i try-catch the values, and possible exceptions - calling different callbacks for different results.
The resources I've found on testing coroutines are all about testing suspended functions, and runBlocking {} is the solution to everything. Except for me it isn't
I made a quick example
#Mock
val mockListener: DoSomething.Listener = mock()
#Test
fun testSomething() {
val doer = DoSomething(mockListener)
runBlocking {
doer.doIt()
verify(mockListener).listen(any())
}
}
class DoSomething(val listener: Listener) {
interface Listener {
fun listen(s: String)
}
fun doIt() {
launch {
listener.listen(theThing().await())
}
}
private fun theThing(): Deferred<String> {
return async {
delay(5, TimeUnit.SECONDS)
return#async "Wow, a thing"
}
}
}
What I want is for the actually run all functions. The test should take 5 seconds minimum, but it just runs through the code in a couple of millisconds- ie. it doesn't block.
I've tried adding
runBlocking {
launch {
// doer.doIt()
}.joinChildren()
}
And similar practices but I just can't get the test to actually wait for my launch inside of another class to finish before the test is finished.
Placing the verify(...) outside of the runBlocking also makes the test fail, which it should.
Any input, helpers, good practice etc. is appreciated!
You can provide the CoroutineContext explicitly for your doIt() function:
fun doIt(context: CoroutineContext = DefaultDispatcher) {
launch(context) {
listener.listen(theThing().await()
}
}
With this parameter you could easily change the coroutine context - in your test code you use the blocking context:
runBlocking {
doer.doIt(coroutineContext)
}
BTW: You don't need to use launch and async. With launch you are in a suspendable context and you don't need to run theThing() asynchronously. Especially if you invoke await() in the next step:
fun doIt(context: CoroutineContext = DefaultDispatcher) {
launch(context) {
listener.listen(theThing())
}
}
private suspend fun theThing(): String {
delay(5, TimeUnit.SECONDS)
return "Wow, a thing"
}
Best way would be not to swallow Job in your doIt() function as you do now.
Instead of
fun doIt() {
launch {
listener.listen(theThing().await())
}
}
Do
fun doIt() = launch {
listener.listen(theThing().await())
}
That way your function will return a coroutine, which you can wait for:
doIt().join()
Better still is to use async() instead of launch()
Another comment is that doIt() should be actually doItAsync(), as suggested by Kotlin guidelines.
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().