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)
    …
24
demandé sur starkej2 2017-04-12 00:14:47

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

45
répondu starkej2 2017-06-28 15:42:22

je viens d'ajouter

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

in @Before annotated method.

17
répondu Jay 2017-10-12 06:27:11

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

4
répondu s-hunter 2018-07-17 21:08:26

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
}
2
répondu David Rawson 2018-03-10 06:38:31

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

1
répondu hitesh 2017-06-24 21:03:13

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.

1
répondu AA_PV 2017-06-27 19:43:15

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();
        }
    });
}
0
répondu sunlover3 2018-05-17 13:54:23