65 votes

Test JUnit Android RxJava 2 - getMainLooper dans Android.os.Looper non simulé RuntimeException

Je rencontre une RuntimeException lorsque j'essaie d'exécuter des tests JUnit pour un présentateur qui utilise l'option observeOn(AndroidSchedulers.mainThread()) .

Comme 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$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)
    …

82voto

blacksh33p Points 941

Cette erreur se produit parce que le planificateur par défaut renvoyé par la fonction AndroidSchedulers.mainThread() est une instance de LooperScheduler et s'appuie 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 Scheduler différent avant que les tests ne soient exécutés. Vous pouvez le faire à l'intérieur d'un @BeforeClass de la manière suivante :

@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 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 de test

public class TestClass {
    @ClassRule public static final RxImmediateSchedulerRule schedulers = new RxImmediateSchedulerRule();

    @Test
    public void testStuff_stuffHappens() {
       ...
    }
}

Ces deux méthodes garantissent que les ordonnanceurs par défaut seront remplacés avant l'exécution de tout test et avant que AndroidSchedulers est accessible.

En remplaçant les ordonnanceurs de RxJava par un ordonnanceur immédiat pour les tests unitaires, on s'assure également que les utilisations de RxJava dans le code testé sont exécutées de manière synchrone, ce qui facilite grandement l'écriture des tests unitaires.

Sources :
https://www.infoq.com/articles/Testing-RxJava2 https://medium.com/@peter.tackage/overriding-rxandroid-schedulers-in-rxjava-2-5561b3d14212

0 votes

Veuillez également consulter ma réponse pour une petite modification qui pourrait être nécessaire si vous rencontrez une StackOverflowError.

0 votes

Mise à jour : vous pouvez utiliser les méthodes RxJavaHooks pour définir les programmateurs. Vous avez également le TestScheduler disponible, ainsi que la simple utilisation de Schedulers.immediate().

0 votes

@NelsonRamirez Je crois que RxJavaHooks a été supprimé dans RxJava 2 et que sa fonctionnalité est maintenant incorporée dans RxJavaPlugins.

48voto

Jay Points 497

Je viens d'ajouter

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

sur @Before méthode annotée.

0 votes

Cela a fonctionné pour moi lorsque je n'avais qu'un seul test qui utilisait des planificateurs, mais cela a échoué si j'avais plusieurs planificateurs. La réponse acceptée fonctionne cependant

1 votes

Cela a fonctionné pour moi après avoir ajouté la règle ci-dessous. @get:Rule var rule : TestRule = InstantTaskExecutorRule()

1 votes

Cela a fonctionné pour moi instantanément sans rien d'autre. Merci !

20voto

s-hunter Points 131

J'obtenais la même erreur en testant LiveData. Lors du test de LiveData, cette InstantTaskExecutorRule est nécessaire en plus de RxImmediateSchedulerRule si la classe testée possède à la fois un thread d'arrière-plan et des 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

0 votes

kotlin y @JvmField tu me sauves :)

1 votes

N'oubliez pas de vérifier que vos dépendances sont toutes soit androidx ou tous android.arch / com.android . Vous ne pouvez pas mélanger les choses et vous perdrez beaucoup de temps à vous demander pourquoi cela ne fonctionne pas =)

13voto

Gent Berani Points 2905

En se basant sur la réponse de @starkej2, avec quelques modifications, la réponse correcte pour Kotlin les développeurs le seraient :

  1. Créer RxImmediateSchedulerRule.kt classe :

,

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. Sur votre classe de test, créez planificateurs ClassRule :

    class TestViewModelTest {
    
    companion object {
       @ClassRule
       @JvmField
       val schedulers = RxImmediateSchedulerRule()
    }
    
    @Before
    fun setUp() {
        //your setup code here
    }
    
    @Test
    fun yourTestMethodHere{}
    }

9voto

David Rawson Points 11480

Comme dans les conseils donnés dans cet article de Medium par Peter Tackage vous pouvez injecter les Schedulers vous-même.

Nous savons tous que l'appel direct de méthodes statiques peut rendre les classes difficiles à tester et si vous utilisez un framework d'injection de dépendances comme Dagger 2, l'injection des Schedulers 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…
}

Définir une mise en œuvre :

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 Schedulers 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 disposez maintenant de tous les avantages de l'utilisation TestScheduler quand vous le souhaitez dans vos tests unitaires. Cela s'avère pratique dans les situations où vous souhaitez 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 Schedulers injectés, les crochets statiques mentionnés dans les autres méthodes peuvent être réalisés en utilisant des lambdas :

@Before
public void setUp() {
    RxAndroidPlugins.setInitMainThreadSchedulerHandler(h -> Schedulers.trampoline());
    RxJavaPlugins.setIoSchedulerHandler(h -> Schedulers.trampoline());
//etc
}

0 votes

C'est la meilleure réponse, les crochets statiques ressemblent à du piratage pour moi. L'injection de dépendances est votre ami

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X