Android RxJava 2 JUnit test - getMainLooper in android.os.Looper not mocked RuntimeException
Solution 1
This error occurs because the default scheduler returned by AndroidSchedulers.mainThread()
is an instance of LooperScheduler
and relies on Android dependencies that are not available in JUnit tests.
We can avoid this issue by initializing RxAndroidPlugins
with a different Scheduler before the tests are run. You can do this inside of a @BeforeClass
method like so:
@BeforeClass
public static void setUpRxSchedulers() {
Scheduler immediate = new Scheduler() {
@Override
public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
// this prevents StackOverflowErrors when scheduling with a delay
return super.scheduleDirect(run, 0, unit);
}
@Override
public Worker createWorker() {
return new ExecutorScheduler.ExecutorWorker(Runnable::run);
}
};
RxJavaPlugins.setInitIoSchedulerHandler(scheduler -> immediate);
RxJavaPlugins.setInitComputationSchedulerHandler(scheduler -> immediate);
RxJavaPlugins.setInitNewThreadSchedulerHandler(scheduler -> immediate);
RxJavaPlugins.setInitSingleSchedulerHandler(scheduler -> immediate);
RxAndroidPlugins.setInitMainThreadSchedulerHandler(scheduler -> immediate);
}
Or you can create a custom TestRule
that will allow you to reuse the initialization logic across multiple test classes.
public class RxImmediateSchedulerRule implements TestRule {
private Scheduler immediate = new Scheduler() {
@Override
public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
// this prevents StackOverflowErrors when scheduling with a delay
return super.scheduleDirect(run, 0, unit);
}
@Override
public Worker createWorker() {
return new ExecutorScheduler.ExecutorWorker(Runnable::run);
}
};
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
RxJavaPlugins.setInitIoSchedulerHandler(scheduler -> immediate);
RxJavaPlugins.setInitComputationSchedulerHandler(scheduler -> immediate);
RxJavaPlugins.setInitNewThreadSchedulerHandler(scheduler -> immediate);
RxJavaPlugins.setInitSingleSchedulerHandler(scheduler -> immediate);
RxAndroidPlugins.setInitMainThreadSchedulerHandler(scheduler -> immediate);
try {
base.evaluate();
} finally {
RxJavaPlugins.reset();
RxAndroidPlugins.reset();
}
}
};
}
}
Which you can then apply to your test class
public class TestClass {
@ClassRule public static final RxImmediateSchedulerRule schedulers = new RxImmediateSchedulerRule();
@Test
public void testStuff_stuffHappens() {
...
}
}
Both of these methods will ensure that the default schedulers will be overridden before any of the tests execute and before AndroidSchedulers
is accessed.
Overriding the RxJava schedulers with an immediate scheduler for unit testing will also make sure the RxJava usages in the code being tested gets run synchronously, which will make it much easier to write the unit tests.
Sources:
https://www.infoq.com/articles/Testing-RxJava2
https://medium.com/@peter.tackage/overriding-rxandroid-schedulers-in-rxjava-2-5561b3d14212
Solution 2
I just added
RxAndroidPlugins.setInitMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
in @Before
annoted method.
Solution 3
I was getting the same error when testing LiveData. When testing LiveData, this InstantTaskExecutorRule is needed in addition to RxImmediateSchedulerRule if the class being tested has both background thread and LiveData.
@RunWith(MockitoJUnitRunner::class)
class MainViewModelTest {
companion object {
@ClassRule @JvmField
val schedulers = RxImmediateSchedulerRule()
}
@Rule
@JvmField
val rule = InstantTaskExecutorRule()
@Mock
lateinit var dataRepository: DataRepository
lateinit var model: MainViewModel
@Before
fun setUp() {
model = MainViewModel(dataRepository)
}
@Test
fun fetchData() {
//given
val returnedItem = createDummyItem()
val observer = mock<Observer<List<Post>>>()
model.getPosts().observeForever(observer)
//when
liveData.value = listOf(returnedItem)
//than
verify(observer).onChanged(listOf(Post(returnedItem.id, returnedItem.title, returnedItem.url)))
}
}
Reference: https://pbochenski.pl/blog/07-12-2017-testing_livedata.html
Solution 4
Based on @starkej2 answer, with some changes, the correct answer for Kotlin developers would be:
- Create
RxImmediateSchedulerRule.kt
class:
,
import io.reactivex.Scheduler
import io.reactivex.android.plugins.RxAndroidPlugins
import io.reactivex.internal.schedulers.ExecutorScheduler
import io.reactivex.plugins.RxJavaPlugins
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import java.util.concurrent.Executor
class RxImmediateSchedulerRule : TestRule {
private val immediate = object : Scheduler() {
override fun createWorker(): Worker {
return ExecutorScheduler.ExecutorWorker(Executor { it.run() })
}
}
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
RxJavaPlugins.setInitIoSchedulerHandler { immediate }
RxJavaPlugins.setInitComputationSchedulerHandler { immediate }
RxJavaPlugins.setInitNewThreadSchedulerHandler { immediate }
RxJavaPlugins.setInitSingleSchedulerHandler { immediate }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { immediate }
try {
base.evaluate()
} finally {
RxJavaPlugins.reset()
RxAndroidPlugins.reset()
}
}
}
}
}
-
On your test class, create schedulers ClassRule:
class TestViewModelTest { companion object { @ClassRule @JvmField val schedulers = RxImmediateSchedulerRule() } @Before fun setUp() { //your setup code here } @Test fun yourTestMethodHere{} }
Solution 5
As in the advice in this Medium article by Peter Tackage you can inject the Schedulers yourself.
We all know that directly calling static methods can make for classes that are hard to test and if you use a dependency injection framework like Dagger 2 injecting the Schedulers can be especially easy. The example is as follows:
Define an interface in your project:
public interface SchedulerProvider {
Scheduler ui();
Scheduler computation();
Scheduler io();
Scheduler special();
// Other schedulers as required…
}
Define an implementation:
final class AppSchedulerProvider implements SchedulerProvider {
@Override
public Scheduler ui() {
return AndroidSchedulers.mainThread();
}
@Override
public Scheduler computation() {
return Schedulers.computation();
}
@Override
public Scheduler io() {
return Schedulers.io();
}
@Override
public Scheduler special() {
return MyOwnSchedulers.special();
}
}
Now instead of using direct references to the Schedulers like this:
bookstoreModel.getFavoriteBook()
.map(Book::getTitle)
.delay(5, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(view::setBookTitle));
You use references to your interface:
bookstoreModel.getFavoriteBook()
.map(Book::getTitle)
.delay(5, TimeUnit.SECONDS,
this.schedulerProvider.computation())
.observeOn(this.schedulerProvider.ui())
.subscribe(view::setBookTitle));
Now for your tests, you could define a TestSchedulersProvider like this:
public final class TestSchedulersProvider implements SchedulerProvider {
@Override
public Scheduler ui() {
return new TestScheduler();
}
@Override
public Scheduler io() {
return Schedulers.trampoline(); //or test scheduler if you want
}
//etc
}
You now have all of the advantages of using TestScheduler
when you want to in your unit tests. This comes in handy for situations where you might want to test a delay:
@Test
public void testIntegerOneIsEmittedAt20Seconds() {
//arrange
TestObserver<Integer> o = delayedRepository.delayedInt()
.test();
//act
testScheduler.advanceTimeTo(20, TimeUnit.SECONDS);
//assert
o.assertValue(1);
}
Otherwise, if you don't want to use injected Schedulers the static hooks mentioned in the other methods can be done using lambdas:
@Before
public void setUp() {
RxAndroidPlugins.setInitMainThreadSchedulerHandler(h -> Schedulers.trampoline());
RxJavaPlugins.setIoSchedulerHandler(h -> Schedulers.trampoline());
//etc
}
starkej2
Updated on July 05, 2022Comments
-
starkej2 almost 2 years
I am encountering a RuntimeException when attempting to run JUnit tests for a presenter that is using
observeOn(AndroidSchedulers.mainThread())
.Since they are pure JUnit tests and not Android instrumentation tests, they don't have access to Android dependencies, causing me to encounter the following error when executing the tests:
java.lang.ExceptionInInitializerError at io.reactivex.android.schedulers.AndroidSchedulers$1.call(AndroidSchedulers.java:35) at io.reactivex.android.schedulers.AndroidSchedulers$1.call(AndroidSchedulers.java:33) at io.reactivex.android.plugins.RxAndroidPlugins.callRequireNonNull(RxAndroidPlugins.java:70) at io.reactivex.android.plugins.RxAndroidPlugins.initMainThreadScheduler(RxAndroidPlugins.java:40) at io.reactivex.android.schedulers.AndroidSchedulers.<clinit>(AndroidSchedulers.java:32) … Caused by: java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details. at android.os.Looper.getMainLooper(Looper.java) at io.reactivex.android.schedulers.AndroidSchedulers$MainHolder.<clinit>(AndroidSchedulers.java:29) ... java.lang.NoClassDefFoundError: Could not initialize class io.reactivex.android.schedulers.AndroidSchedulers at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) …
-
AA_PV about 7 yearspls also check my answer for a small change which might be required if you run into StackOverflowError
-
starkej2 about 7 yearsThanks! I actually ran into that same issue shortly after posting my answer. Had a similar solution as you...I'll update the answer since this info is probably useful for people.
-
Nelson Ramirez almost 7 yearsUpdate: You can use the RxJavaHooks methods to set the schedulers. Also you have the TestScheduler available, as well as simply using Schedulers.immediate()
-
starkej2 almost 7 years@NelsonRamirez I believe RxJavaHooks was removed in RxJava 2 and it's functionality is now incorporated into RxJavaPlugins
-
Ahmed Mostafa almost 6 years
kotlin
and@JvmField
, you save me :) -
Etienne Lawlor over 5 yearsSo i did something similar in a Kotlin project and it works but for whatever reason in a Java project the unit tests will only fail if you run it through ./gradlew test but it passes when you click run in the gutter next to the test class name in Android Studio. I thought it runs the test the same way. Does anyone know about this nuance ?
-
JDenais over 5 yearsDon't forget to check that your dependencies are all either
androidx
or allandroid.arch
/com.android
. You can't mix and match and you will lose a lot of time wondering why it's not working =) -
ericn over 5 yearsThis is the best answer, static hooks look like hack to me. Dependency injection is your friend
-
Subhrajyoti Sen over 5 yearsThis worked for me when I had only 1 test that used Schedulers but it failed if I had multiple schedulers. The accepted answer works though
-
Sam over 4 yearsthis worked for me after adding the below rule. @get:Rule var rule: TestRule = InstantTaskExecutorRule()
-
A. Steenbergen over 4 yearsThis worked for me instantly without anything else required. Thank you!
-
Mario Codes about 4 yearsThis did it for me. None of the above solutions were good for me, as they don't include the imports (as always), and as someone new to Android I don't know the frameworks they reference to.
-
fahrizal89 over 2 yearsjust using
@Rule @JvmField val rule = InstantTaskExecutorRule()
it saved my day. thank you!