web-dev-qa-db-fra.com

Test d'Android RxJava 2 JUnit - getMainLooper dans Android.os.Looper non moqué de RuntimeException

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

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

59
starkej2

Je viens d'ajouter 

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

dans la méthode annotée @Before.

24
Jay

Sur la base de la réponse @ starkej2, avec quelques modifications, la réponse correcte pour Kotlin developers serait:

  1. Créer la classe 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()
                }
            }
        }
    }
}
  1. 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{}
    }
    
8
Gent Berani

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

6
s-hunter

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

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

1
hitesh

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.

1
AA_PV

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