android - testobserver - test observer rxjava
Prueba de Android RxJava 2 JUnit-getMainLooper en android.os.Looper no se burla de RuntimeException (7)
Estoy encontrando una excepción RuntimeException cuando intento ejecutar pruebas JUnit para un presentador que está utilizando observeOn(AndroidSchedulers.mainThread())
.
Dado que son pruebas de JUnit puras y no pruebas de instrumentación de Android, no tienen acceso a las dependencias de Android, lo que me hace encontrar el siguiente error al ejecutar las pruebas:
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)
…
Acabo de añadir
RxAndroidPlugins.setInitMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
en @Before
método @Before
.
Como en el consejo de este artículo de Medium por Peter Tackage , puede inyectar los Programadores usted mismo.
Todos sabemos que llamar directamente a los métodos estáticos puede hacer que las clases sean difíciles de probar y si utiliza un marco de inyección de dependencia como Dagger 2, la inyección de los Programadores puede ser especialmente fácil. El ejemplo es el siguiente:
Defina una interfaz en su proyecto:
public interface SchedulerProvider {
Scheduler ui();
Scheduler computation();
Scheduler io();
Scheduler special();
// Other schedulers as required…
}
Definir una implementación:
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();
}
}
Ahora, en lugar de usar referencias directas a los programadores como este:
bookstoreModel.getFavoriteBook()
.map(Book::getTitle)
.delay(5, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(view::setBookTitle));
Utiliza referencias a su interfaz:
bookstoreModel.getFavoriteBook()
.map(Book::getTitle)
.delay(5, TimeUnit.SECONDS,
this.schedulerProvider.computation())
.observeOn(this.schedulerProvider.ui())
.subscribe(view::setBookTitle));
Ahora para sus pruebas, podría definir un proveedor de pruebas de planificación como este:
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
}
Ahora tiene todas las ventajas de usar TestScheduler
cuando quiera en sus pruebas unitarias. Esto es útil para situaciones en las que es posible que desee probar un retraso:
@Test
public void testIntegerOneIsEmittedAt20Seconds() {
//arrange
TestObserver<Integer> o = delayedRepository.delayedInt()
.test();
//act
testScheduler.advanceTimeTo(20, TimeUnit.SECONDS);
//assert
o.assertValue(1);
}
De lo contrario, si no desea utilizar los Programadores inyectados, los enlaces estáticos mencionados en los otros métodos se pueden hacer utilizando lambdas:
@Before
public void setUp() {
RxAndroidPlugins.setInitMainThreadSchedulerHandler(h -> Schedulers.trampoline());
RxJavaPlugins.setIoSchedulerHandler(h -> Schedulers.trampoline());
//etc
}
Este error se produce porque el programador predeterminado devuelto por AndroidSchedulers.mainThread()
es una instancia de LooperScheduler
y depende de las dependencias de Android que no están disponibles en las pruebas de JUnit.
Podemos evitar este problema al inicializar RxAndroidPlugins
con un Programador diferente antes de que se ejecuten las pruebas. Puedes hacer esto dentro de un método @BeforeClass
así:
@BeforeClass
public static void setUpRxSchedulers() {
Scheduler immediate = new Scheduler() {
@Override
public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
// this prevents Errors 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);
}
O puede crear una TestRule
personalizada que le permitirá reutilizar la lógica de inicialización en múltiples clases de prueba.
public class RxImmediateSchedulerRule implements TestRule {
private Scheduler immediate = new Scheduler() {
@Override
public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
// this prevents Errors 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 luego puede aplicar a su clase de prueba
public class TestClass {
@ClassRule public static final RxImmediateSchedulerRule schedulers = new RxImmediateSchedulerRule();
@Test
public void testStuff_stuffHappens() {
...
}
}
Ambos métodos asegurarán que los programadores predeterminados se anularán antes de que se ejecute cualquiera de las pruebas y antes de que se acceda a AndroidSchedulers
.
Al anular los programadores de RxJava con un programador inmediato para la prueba de unidad también se asegurará de que los usos de RxJava en el código que se está probando se ejecuten de manera sincrónica, lo que facilitará mucho la escritura de las pruebas de unidad.
Fuentes:
https://www.infoq.com/articles/Testing-RxJava2 https://medium.com/@peter.tackage/overriding-rxandroid-schedulers-in-rxjava-2-5561b3d14212
Para RxJava 1 puedes crear diferentes planificadores como este:
@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();
}
Unidad de prueba de aplicaciones de Android con actualización y rxjava.
Recibía el mismo error al probar LiveData. Al probar LiveData, se necesita esta InstantTaskExecutorRule además de RxImmediateSchedulerRule si la clase que se está probando tiene un hilo de fondo y 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)))
}
}
Referencia: https://pbochenski.pl/blog/07-12-2017-testing_livedata.html
Solo para agregar a la respuesta de starkej2, funcionó muy bien para mí hasta que encontré error al probar un Observable.timer (). No hay ayuda en eso, pero afortunadamente lo conseguí trabajando con la siguiente definición de Programador, con todas las demás pruebas que también pasaron.
new Scheduler() {
@Override
public Worker createWorker() {
return new ExecutorScheduler.ExecutorWorker(new ScheduledThreadPoolExecutor(1) {
@Override
public void execute(@NonNull Runnable runnable) {
runnable.run();
}
});
}
};
Descansa como en la respuesta de starkej2. Espero que esto ayude a alguien.
Tuve este problema y llegué a esta publicación, pero no pude encontrar nada para RX 1. Así que esta es la solución si tiene el mismo problema en la primera versión.
@BeforeClass
public static void setupClass() {
RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
@Override
public Scheduler getMainThreadScheduler() {
return Schedulers.trampoline();
}
});
}