Подтвердить что ты не робот

Android RxJava 2 JUnit test - 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)
    …
4b9b3361

Ответ 1

Эта ошибка возникает из-за того, что планировщик по умолчанию, возвращаемый 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 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);
}

Или вы можете создать пользовательский 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 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();
                }
            }
        };
    }
}

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

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

Ответ 2

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

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

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

Ответ 3

Я получал ту же ошибку при тестировании 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

Ответ 4

Исходя из ответа @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{}
    }
    

Ответ 5

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

Мы все знаем, что прямой вызов статических методов может создать классы, которые трудно тестировать, и если вы используете среду внедрения зависимостей, такую как 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
}

Ответ 6

Для 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

Ответ 7

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

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

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

Ответ 8

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

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

Ответ 9

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

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

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

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

@get:Rule
val schedulers = RxImmediateSchedulerRule()

Ответ 10

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

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