Тест Android RxJava 2 JUnit - getMainLooper в android.os.Looper не высмеивается RuntimeException

Я сталкиваюсь с RuntimeException при попытке запустить тесты JUnit для докладчика, который использует observeOn(AndroidSchedulers.mainThread()),

Поскольку они являются чистыми тестами JUnit, а не инструментальными тестами Android, у них нет доступа к зависимостям Android, что приводит к следующей ошибке при выполнении тестов:

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

8 ответов

Решение

Эта ошибка возникает из-за того, что планировщик по умолчанию возвращается AndroidSchedulers.mainThread() это пример LooperScheduler и полагается на зависимости Android, которые недоступны в тестах JUnit.

Мы можем избежать этой проблемы, инициализируя RxAndroidPlugins с другим планировщиком до запуска тестов. Вы можете сделать это внутри @BeforeClass метод вроде так:

@BeforeClass
public static void setUpRxSchedulers() {
    Scheduler immediate = new Scheduler() {
        @Override
        public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
            // this prevents StackruErrors 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);
}

Или вы можете создать кастом TestRule это позволит вам повторно использовать логику инициализации в нескольких тестовых классах.

public class RxImmediateSchedulerRule implements TestRule {
    private Scheduler immediate = new Scheduler() {
        @Override
        public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
            // this prevents StackruErrors 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();
                }
            }
        };
    }
}

Что вы можете затем применить к своему тестовому классу

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

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

Оба эти метода гарантируют, что планировщики по умолчанию будут переопределены до выполнения любого из тестов и до AndroidSchedulers Доступ

Переопределение планировщиков RxJava непосредственным планировщиком для модульного тестирования также обеспечит синхронное выполнение операций RxJava в тестируемом коде, что значительно облегчит написание модульных тестов.

Источники:
https://www.infoq.com/articles/Testing-RxJava2 https://medium.com/@peter.tackage/overriding-rxandroid-schedulers-in-rxjava-2-5561b3d14212

Я только добавил

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

в @Before аннотированный метод.

Я получал ту же ошибку при тестировании LiveData. При тестировании LiveData этот InstantTaskExecutorRule необходим в дополнение к RxImmediateSchedulerRule, если у тестируемого класса есть как фоновый поток, так и 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)))
    }

}

Ссылка: https://pbochenski.pl/blog/07-12-2017-testing_livedata.html

Исходя из ответа @starkej2, с некоторыми изменениями правильный ответ для разработчиков Kotlin будет:

  1. Создайте 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. В вашем тестовом классе создайте планировщики ClassRule:

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

Как и в советах в этой статье Питера Тэкэджа, вы можете вводить планировщики самостоятельно.

Мы все знаем, что прямой вызов статических методов может создать классы, которые трудно тестировать, и если вы используете среду внедрения зависимостей, такую ​​как Dagger 2, внедрение планировщиков может быть особенно простым. Пример выглядит следующим образом:

Определите интерфейс в вашем проекте:

public interface SchedulerProvider {
    Scheduler ui();
    Scheduler computation();
    Scheduler io();
    Scheduler special();
    // Other schedulers as required…
}

Определите реализацию:

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

Теперь вместо использования прямых ссылок на планировщики, как это:

 bookstoreModel.getFavoriteBook()
               .map(Book::getTitle)
               .delay(5, TimeUnit.SECONDS)
               .observeOn(AndroidSchedulers.mainThread())
               .subscribe(view::setBookTitle));

Вы используете ссылки на ваш интерфейс:

bookstoreModel.getFavoriteBook()
          .map(Book::getTitle)
          .delay(5, TimeUnit.SECONDS, 
                 this.schedulerProvider.computation())
          .observeOn(this.schedulerProvider.ui())
          .subscribe(view::setBookTitle));

Теперь для ваших тестов вы можете определить TestSchedulersProvider следующим образом:

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
}

Теперь у вас есть все преимущества использования TestScheduler когда вы хотите в ваших модульных тестах. Это удобно для ситуаций, когда вы можете проверить задержку:

@Test
public void testIntegerOneIsEmittedAt20Seconds() {
    //arrange
    TestObserver<Integer> o = delayedRepository.delayedInt()
            .test();

    //act
    testScheduler.advanceTimeTo(20, TimeUnit.SECONDS);

    //assert
    o.assertValue(1);
}

В противном случае, если вы не хотите использовать встроенные планировщики, статические перехватчики, упомянутые в других методах, могут быть выполнены с помощью лямбда-выражений:

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

Если у вас все еще есть проблема, и ни один из приведенных выше кодов вам не помогает, кроме этого, неплохо добавить эту строку в файл app.gradle:

testOptions {
    animationsDisabled = true
    unitTests {
        includeAndroidResources = true
        returnDefaultValues = true
    }
}

Для тех, кто делает с Kotlin и используя Rule вместо создания companion object ты можешь использовать @get:Rule,

Поэтому вместо использования:

companion object {
 @ClassRule
 @JvmField
 val schedulers = RxImmediateSchedulerRule()
}

Вы можете просто использовать:

@get:Rule
val schedulers = RxImmediateSchedulerRule()

Просто чтобы добавить к ответу starkej2, он работал очень хорошо, пока я не столкнулся с stackruerror при тестировании Observable.timer(). В этом нет никакой помощи, но, к счастью, я работал с приведенным ниже определением планировщика, причем все остальные тесты также прошли.

new Scheduler() {
            @Override
            public Worker createWorker() {
                return new ExecutorScheduler.ExecutorWorker(new ScheduledThreadPoolExecutor(1) {
                    @Override
                    public void execute(@NonNull Runnable runnable) {
                        runnable.run();
                    }
                });
            }
        };

Отдыхайте как в ответе starkej2. Надеюсь, это кому-нибудь поможет.

У меня была эта проблема и я пришел к этому сообщению, но я не смог найти ничего для RX 1. Так что это решение, если у вас та же проблема с первой версией.

@BeforeClass
public static void setupClass() {
    RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
        @Override
        public Scheduler getMainThreadScheduler() {
            return Schedulers.trampoline();
        }
    });
}

Для RxJava 1 вы можете создавать различные планировщики, как это:

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

Модульное тестирование Android-приложения с модификацией и rxjava

Другие вопросы по тегам