RxJava in the unit-tests can swallow the exception which would crash the real Android app. Here is how to fix the tests.
Suppose we’re using RxJava in our Android app, and we cover it with unit-tests. Does it mean the same code that passes the test won’t crash the app in production on the same input data? No! But why?
Let’s check the simplified example. The
waitAndFail function emulates some background work and then it throws an exception. In theory, this test should fail, in practice, it prints an error message to the console and finished successfully. The same function used in the Android app will crash it.
But we’ll see something like that in the console
io.reactivex.exceptions.OnErrorNotImplementedException: The exception was not handled due to missing onError handler in the subscribe() method call. Further reading: https://github.com/ReactiveX/RxJava/wiki/Error-Handling | java.lang.IllegalStateException
The reason is that ART (and Dalvik) virtual machine, that runs the app, and local JVM, that runs the tests, have different uncaught error handlers. In Java, each thread has one, and when the exception happens in the thread, JVM calls this thread’s handler. The default behavior for Android is to log the exception and terminate the process, while the default desktop JVM’s error handler prints the exception and ignores it. But if we get the exception in the test function it should fail, shouldn’t it? Yes, but RxJava doesn’t rethrow exceptions it captures and calls the tread’s handler directly, instead.
So how to fix that? How to make code under unit-tests crash when (and only when) the application would crash give correct feedback as early as possible?
JUnit allows us to add rules to the test to execute arbitrary code before and after each test, we can use that to install a scheduler that actually crashes to the main thread and to ensure the is executed in the same thread we can install RxJava’s handy trampoline scheduler. Now all our asynchronous code becomes synchronous, so the test will not finish before it actually gets to the exception and it will not ignore the exception anymore.
Then this rule can be used in any test (and in combination with different rules) as
val rxRule = RxSchedulersTrampolineTestRule()
The problem may sound a bit artificial, but in real life developers often forget
onError function in
subscribe or throw the exception from the wrong place, and if the test is not designed for such case the exception will slip through. So a strict rule serves as an extra guard rail, that can highlight the problem with the code or with the test itself.