Android RxJava 2 JUnit test-getMainLooper dans android.OS.Looper Non moqué RuntimeException
je rencontre une RuntimeException en essayant d'exécuter des tests JUnit pour un présentateur qui utilise observeOn(AndroidSchedulers.mainThread())
.
Puisqu'il s'agit de tests JUnit purs et non de tests D'instrumentation Android, ils n'ont pas accès aux dépendances Android, ce qui me fait rencontrer l'erreur suivante lors de l'exécution des tests:
java.lang.ExceptionInInitializerError
at io.reactivex.android.schedulers.AndroidSchedulers.call(AndroidSchedulers.java:35)
at io.reactivex.android.schedulers.AndroidSchedulers.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)
…
7 réponses
cette erreur se produit parce que l'ordonnanceur par défaut retourné par AndroidSchedulers.mainThread()
est une instance de LooperScheduler
et repose sur des dépendances Android qui ne sont pas disponibles dans les tests JUnit.
nous pouvons éviter ce problème en initialisant RxAndroidPlugins
avec un programmeur différent avant que les essais ne soient exécutés. Vous pouvez le faire à l'intérieur d'une méthode @BeforeClass
comme ainsi:
@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);
}
ou vous pouvez créer un personnalisé TestRule
qui vous permettra de réutiliser la logique d'initialisation dans plusieurs classes de test.
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();
}
}
};
}
}
que vous pouvez ensuite appliquer à votre classe d'essai
public class TestClass {
@ClassRule public static final RxImmediateSchedulerRule schedulers = new RxImmediateSchedulerRule();
@Test
public void testStuff_stuffHappens() {
...
}
}
ces deux méthodes permettent de s'assurer que les ordonnanceurs par défaut seront annulés avant l'exécution de l'un des tests et avant l'accès à AndroidSchedulers
.
surpasser les programmeurs RxJava avec un programmeur immédiat pour les tests unitaires permettra également de s'assurer que les usages RxJava dans le code être testé est exécuté de manière synchrone, ce qui facilitera grandement la rédaction des tests unitaires.
Sources:
https://www.infoq.com/articles/Testing-RxJava2
https://medium.com/@peter.tackage/overriding-rxandroid-schedulers-in-rxjava-2-5561b3d14212
je viens d'ajouter
RxAndroidPlugins.setInitMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
in @Before
annotated method.
j'ai eu la même erreur quand J'ai testé LiveData. Lors de L'essai de LiveData, ce InstantTaskExecutorRule est nécessaire en plus de Rximmediateschedulerrulle si la classe à l'essai a à la fois le fil de fond et 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)))
}
}
référence: https://pbochenski.pl/blog/07-12-2017-testing_livedata.html
comme dans le conseil dans cet article médium de Peter Tackage vous pouvez injecter les programmeurs vous-même.
nous savons tous que l'appel direct de méthodes statiques peut rendre pour les classes qui sont difficiles à tester et si vous utilisez un cadre d'injection de dépendance comme Dagger 2 injecter les ordonnanceurs peut être particulièrement facile. L'exemple est le suivant:
définissez une interface dans votre projet:
public interface SchedulerProvider {
Scheduler ui();
Scheduler computation();
Scheduler io();
Scheduler special();
// Other schedulers as required…
}
definit 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();
}
}
maintenant au lieu d'utiliser des références directes aux ordonnanceurs comme ceci:
bookstoreModel.getFavoriteBook()
.map(Book::getTitle)
.delay(5, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(view::setBookTitle));
vous utilisez des références à votre interface:
bookstoreModel.getFavoriteBook()
.map(Book::getTitle)
.delay(5, TimeUnit.SECONDS,
this.schedulerProvider.computation())
.observeOn(this.schedulerProvider.ui())
.subscribe(view::setBookTitle));
maintenant pour vos tests, vous pouvez définir un TestSchedulersProvider comme ceci:
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
}
vous avez maintenant tous les avantages d'utiliser TestScheduler
quand vous voulez dans votre unité fait des tests. Cela est pratique pour les situations où vous pourriez vouloir tester un délai:
@Test
public void testIntegerOneIsEmittedAt20Seconds() {
//arrange
TestObserver<Integer> o = delayedRepository.delayedInt()
.test();
//act
testScheduler.advanceTimeTo(20, TimeUnit.SECONDS);
//assert
o.assertValue(1);
}
Sinon, si vous ne voulez pas utiliser des programmateurs injectés les crochets statiques mentionnés dans les autres méthodes peuvent être faites en utilisant lambdas:
@Before
public void setUp() {
RxAndroidPlugins.setInitMainThreadSchedulerHandler(h -> Schedulers.trampoline());
RxJavaPlugins.setIoSchedulerHandler(h -> Schedulers.trampoline());
//etc
}
pour RxJava 1 vous pouvez créer différents ordonnanceurs comme ceci:
@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();
}
"151940920 de tests Unitaires de l'application android, avec de rénovation et de rxjava
juste pour ajouter à la réponse de starkej2, ça a très bien fonctionné pour moi jusqu'à ce que je tombe sur stackoverflowerror en testant un Observable.minuterie.)( Il n'y a pas d'aide sur cela, mais heureusement je l'ai obtenu travailler avec la définition de planificateur ci-dessous, avec tous les autres tests également passer.
new Scheduler() {
@Override
public Worker createWorker() {
return new ExecutorScheduler.ExecutorWorker(new ScheduledThreadPoolExecutor(1) {
@Override
public void execute(@NonNull Runnable runnable) {
runnable.run();
}
});
}
};
reste comme dans la réponse de starkej2. Espérons que cela aide quelqu'un.
j'ai eu ce problème et je suis venu à ce poste, mais je ne pouvais rien trouver pour RX 1. C'est donc la solution si vous avez le même problème sur la première version.
@BeforeClass
public static void setupClass() {
RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
@Override
public Scheduler getMainThreadScheduler() {
return Schedulers.trampoline();
}
});
}