Je rencontre une exception RuntimeException lorsque j'essaie d'exécuter des tests JUnit pour un présentateur utilisant observeOn(AndroidSchedulers.mainThread())
.
S'agissant de tests JUnit purs et non de tests d'instrumentation Android, ils n'ont pas accès aux dépendances d'Android, ce qui m'a amené à rencontrer l'erreur suivante lors de l'exécution des 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)
…
Cette erreur se produit car le planificateur par défaut renvoyé par AndroidSchedulers.mainThread()
est une instance de LooperScheduler
et repose sur des dépendances Android non disponibles dans les tests JUnit.
Nous pouvons éviter ce problème en initialisant RxAndroidPlugins
avec un autre planificateur avant l'exécution des tests. Vous pouvez faire cela dans une méthode @BeforeClass
comme ceci:
@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);
}
Vous pouvez également créer une variable TestRule
personnalisée 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 de test
public class TestClass {
@ClassRule public static final RxImmediateSchedulerRule schedulers = new RxImmediateSchedulerRule();
@Test
public void testStuff_stuffHappens() {
...
}
}
Ces deux méthodes garantiront que les planificateurs par défaut seront remplacés avant l'exécution d'un test et avant l'accès à AndroidSchedulers
.
Remplacer les ordonnanceurs RxJava par un ordonnanceur immédiat pour les tests unitaires permettra également de s'assurer que les utilisations de RxJava dans le code testé sont exécutées de manière synchrone, ce qui facilitera grandement l'écriture des tests unitaires.
Sources:
https://www.infoq.com/articles/Testing-RxJava2https://medium.com/@peter.tackage/overriding-rxandroid-schedulers-in-rxjava- 2-5561b3d14212
Je viens d'ajouter
RxAndroidPlugins.setInitMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
dans la méthode annotée @Before
.
Sur la base de la réponse @ starkej2, avec quelques modifications, la réponse correcte pour Kotlin developers serait:
RxImmediateSchedulerRule.kt
: ,
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()
}
}
}
}
}
Sur votre classe de test, créez schedulers ClassRule:
class TestViewModelTest {
companion object {
@ClassRule
@JvmField
val schedulers = RxImmediateSchedulerRule()
}
@Before
fun setUp() {
//your setup code here
}
@Test
fun yourTestMethodHere{}
}
J'avais la même erreur en testant LiveData. Lors du test de LiveData, InstantTaskExecutorRule est nécessaire en plus de RxImmediateSchedulerRule si la classe en cours de test comporte à la fois un thread en arrière-plan 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 les conseils donnés dans cet article de Peter Tackage , vous pouvez injecter vous-même les planificateurs.
Nous savons tous que l'appel direct de méthodes statiques peut créer des classes difficiles à tester. Si vous utilisez un framework d'injection de dépendances tel que Dagger 2, l'injection des planificateurs peut s'avérer particulièrement simple. 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…
}
Définir une implémentation:
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
lorsque vous le souhaitez dans vos tests unitaires. Ceci est pratique pour les situations dans lesquelles vous pouvez 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 souhaitez pas utiliser de planificateurs injectés, les points d'ancrage statiques mentionnés dans les autres méthodes peuvent être créés à l'aide de 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 celui-ci:
@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();
}
Tests unitaires de l'application Android avec mise à niveau et rxjava
Pour ajouter à la réponse de starkej2, cela a très bien fonctionné pour moi jusqu'à ce que je rencontre un problème avec stackoverflowerror lors du test d'un observable.timer (). Il n'y a aucune aide à ce sujet mais heureusement, je l'ai obtenu avec la définition ci-dessous du planificateur, tous les autres tests réussissant également.
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. J'espère que ça aide quelqu'un.
J'avais ce problème et je suis arrivé à ce billet, mais je n'ai rien trouvé pour RX 1 . C'est donc la solution si vous rencontrez le même problème avec la première version.
@BeforeClass
public static void setupClass() {
RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
@Override
public Scheduler getMainThreadScheduler() {
return Schedulers.trampoline();
}
});
}