webentwicklung-frage-antwort-db.com.de

Android RxJava 2 JUnit Test - getMainLooper in Android.os.Looper nicht verspottet RuntimeException

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)
    …
33
starkej2

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

59
starkej2

Ich habe gerade hinzugefügt 

RxAndroidPlugins.setInitMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());

in @Before annotierte Methode.

24
Jay

Basierend auf der Antwort von @ starkej2, mit einigen Änderungen, wäre die richtige Antwort für Kotlin Entwickler:

  1. 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()
                }
            }
        }
    }
}
  1. 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{}
    }
    
8
Gent Berani

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

6
s-hunter

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
}
4
David Rawson

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

Unit-Test Android-Anwendung mit Retrofit und Rxjava

1
hitesh

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.

1
AA_PV

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()
0
Skizo-ozᴉʞS

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();
        }
    });
}
0
sunlover3