Beim Versuch, JUnit-Tests für einen Presenter auszuführen, der observeOn(AndroidSchedulers.mainThread())
verwendet, ist eine RuntimeException aufgetreten.
Da es sich um reine JUnit-Tests und nicht um Android-Instrumentationstests handelt, haben sie keinen Zugriff auf Android-Abhängigkeiten, sodass bei der Ausführung der Tests der folgende Fehler angezeigt wird:
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)
…
Dieser Fehler tritt auf, weil der von AndroidSchedulers.mainThread()
zurückgegebene Standardzeitplaner eine Instanz von LooperScheduler
ist und auf Android-Abhängigkeiten beruht, die in JUnit-Tests nicht verfügbar sind.
Wir können dieses Problem vermeiden, indem Sie RxAndroidPlugins
mit einem anderen Scheduler initialisieren, bevor die Tests ausgeführt werden. Sie können dies innerhalb einer @BeforeClass
-Methode wie folgt tun:
@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);
}
Oder Sie können ein benutzerdefiniertes TestRule
erstellen, mit dem Sie die Initialisierungslogik für mehrere Testklassen wiederverwenden können.
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();
}
}
};
}
}
Die können Sie dann auf Ihre Testklasse anwenden
public class TestClass {
@ClassRule public static final RxImmediateSchedulerRule schedulers = new RxImmediateSchedulerRule();
@Test
public void testStuff_stuffHappens() {
...
}
}
Beide Methoden stellen sicher, dass die Standard-Scheduler vor der Ausführung der Tests und vor dem Zugriff auf AndroidSchedulers
überschrieben werden.
Durch das Überschreiben der RxJava-Scheduler mit einem sofortigen Scheduler für Komponententests wird auch sichergestellt, dass die RxJava-Verwendungen im getesteten Code synchron ausgeführt werden, wodurch das Schreiben der Komponententests erheblich vereinfacht wird.
Quellen:
https://www.infoq.com/articles/Testing-RxJava2https://medium.com/@peter.tackage/overriding-rxandroid-schedulers-in-rxjava- 2-5561b3d14212
Ich habe gerade hinzugefügt
RxAndroidPlugins.setInitMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
in @Before
annotierte Methode.
Basierend auf der Antwort von @ starkej2, mit einigen Änderungen, wäre die richtige Antwort für Kotlin Entwickler:
RxImmediateSchedulerRule.kt
-Klasse erstellen: .
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()
}
}
}
}
}
Erstellen Sie in Ihrer Testklasse Scheduler ClassRule:
class TestViewModelTest {
companion object {
@ClassRule
@JvmField
val schedulers = RxImmediateSchedulerRule()
}
@Before
fun setUp() {
//your setup code here
}
@Test
fun yourTestMethodHere{}
}
Beim Testen von LiveData wurde der gleiche Fehler angezeigt. Beim Testen von LiveData wird diese InstantTaskExecutorRule zusätzlich zu RxImmediateSchedulerRule benötigt, wenn die getestete Klasse sowohl einen Hintergrundthread als auch LiveData hat.
@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)))
}
}
Referenz: https://pbochenski.pl/blog/07-12-2017-testing_livedata.html
Wie in den Ratschlägen in diesem Medium-Artikel von Peter Tackage können Sie die Scheduler selbst spritzen.
Wir alle wissen, dass durch das direkte Aufrufen statischer Methoden schwer zu testende Klassen erstellt werden können. Wenn Sie ein Abhängigkeitsinjektions-Framework wie Dagger 2 verwenden, ist das Einfügen der Scheduler besonders einfach. Das Beispiel ist wie folgt:
Definieren Sie eine Schnittstelle in Ihrem Projekt:
public interface SchedulerProvider {
Scheduler ui();
Scheduler computation();
Scheduler io();
Scheduler special();
// Other schedulers as required…
}
Definieren Sie eine Implementierung:
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();
}
}
Statt direkte Verweise auf die Scheduler wie folgt zu verwenden:
bookstoreModel.getFavoriteBook()
.map(Book::getTitle)
.delay(5, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(view::setBookTitle));
Sie verwenden Verweise auf Ihre Schnittstelle:
bookstoreModel.getFavoriteBook()
.map(Book::getTitle)
.delay(5, TimeUnit.SECONDS,
this.schedulerProvider.computation())
.observeOn(this.schedulerProvider.ui())
.subscribe(view::setBookTitle));
Nun könnten Sie für Ihre Tests einen TestSchedulersProvider wie folgt definieren:
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
}
Sie haben jetzt alle Vorteile der Verwendung von TestScheduler
, wenn Sie möchten, dass Sie in Ihren Gerätetests arbeiten. Dies ist praktisch für Situationen, in denen Sie eine Verzögerung testen möchten:
@Test
public void testIntegerOneIsEmittedAt20Seconds() {
//arrange
TestObserver<Integer> o = delayedRepository.delayedInt()
.test();
//act
testScheduler.advanceTimeTo(20, TimeUnit.SECONDS);
//assert
o.assertValue(1);
}
Wenn Sie keine eingespritzten Scheduler verwenden möchten, können die statischen Hooks, die in den anderen Methoden erwähnt werden, auch mit Lambdas ausgeführt werden:
@Before
public void setUp() {
RxAndroidPlugins.setInitMainThreadSchedulerHandler(h -> Schedulers.trampoline());
RxJavaPlugins.setIoSchedulerHandler(h -> Schedulers.trampoline());
//etc
}
Für RxJava 1 können Sie verschiedene Scheduler wie folgt erstellen:
@Before
public void setUp() throws Exception {
// Override RxJava schedulers
RxJavaHooks.setOnIOScheduler(new Func1<Scheduler, Scheduler>() {
@Override
public Scheduler call(Scheduler scheduler) {
return Schedulers.immediate();
}
});
RxJavaHooks.setOnComputationScheduler(new Func1<Scheduler, Scheduler>() {
@Override
public Scheduler call(Scheduler scheduler) {
return Schedulers.immediate();
}
});
RxJavaHooks.setOnNewThreadScheduler(new Func1<Scheduler, Scheduler>() {
@Override
public Scheduler call(Scheduler scheduler) {
return Schedulers.immediate();
}
});
// Override RxAndroid schedulers
final RxAndroidPlugins rxAndroidPlugins = RxAndroidPlugins.getInstance();
rxAndroidPlugins.registerSchedulersHook(new RxAndroidSchedulersHook() {
@Override
public Scheduler getMainThreadScheduler() {
return Schedulers.immediate();
}
});
}
@After
public void tearDown() throws Exception {
RxJavaHooks.reset();
RxAndroidPlugins.getInstance().reset();
}
Nur um die Antwort von starkej2 zu ergänzen, hat es sehr gut für mich funktioniert, bis ich beim Testen eines Observable.timer () auf stackoverflowerror gestoßen bin. Es gibt keine Hilfe, aber zum Glück habe ich es mit der folgenden Scheduler-Definition gemacht, wobei alle anderen Tests ebenfalls bestanden haben.
new Scheduler() {
@Override
public Worker createWorker() {
return new ExecutorScheduler.ExecutorWorker(new ScheduledThreadPoolExecutor(1) {
@Override
public void execute(@NonNull Runnable runnable) {
runnable.run();
}
});
}
};
Ausruhen wie in der Antwort von starkej2. Hoffe das hilft jemandem.
Für diejenigen, die mit Kotlin
arbeiten und Rule
verwenden, anstatt companion object
Sie können @get:Rule
.
Anstatt also:
companion object {
@ClassRule
@JvmField
val schedulers = RxImmediateSchedulerRule()
}
Sie können einfach verwenden:
@get:Rule
val schedulers = RxImmediateSchedulerRule()
Ich hatte dieses Problem und kam zu diesem Beitrag, aber ich konnte nichts für RX 1 finden ... Dies ist die Lösung, wenn Sie das gleiche Problem in der ersten Version haben.
@BeforeClass
public static void setupClass() {
RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
@Override
public Scheduler getMainThreadScheduler() {
return Schedulers.trampoline();
}
});
}