web-dev-qa-db-ja.com

Android RxJava 2 JUnitテスト-Android.os.LooperのgetMainLooperがRuntimeExceptionをモックされていません

observeOn(AndroidSchedulers.mainThread())を使用しているプレゼンターに対してJUnitテストを実行しようとすると、RuntimeExceptionが発生します。

これらはAndroidインスツルメンテーションテストではなく純粋なJUnitテストであるため、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)
    …
47
starkej2

このエラーは、AndroidSchedulers.mainThread()によって返されるデフォルトのスケジューラがLooperSchedulerのインスタンスであり、JUnitテストでは利用できないAndroid依存関係に依存するために発生します。

テストを実行する前に別のスケジューラで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-RxJava2https://medium.com/@peter.tackage/overriding-rxandroid-schedulers- in-rxjava-2-5561b3d14212

64
starkej2

私はちょうど追加しました

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

@Before注釈付きメソッド。

32
Jknair

LiveDataのテスト時に同じエラーが発生していました。 LiveDataをテストする場合、テストするクラスにバックグラウンドスレッドとLiveDataの両方がある場合、 RxImmediateSchedulerRule に加えて、この InstantTaskExecutorRule が必要です。

@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

13
s-hunter

@ 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. テストクラスでschedulers ClassRuleを作成します。

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

Peter Tackageによるこの中記事 のアドバイスのように、自分でスケジューラーを挿入できます。

静的メソッドを直接呼び出すと、テストが困難なクラスを作成できること、そして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
David Rawson

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

レトロフィットとrxjavaを使用したAndroidアプリケーションの単体テスト

1
hitesh

Starkej2の答えに追加するだけで、Observable.timer()をテストするときにstackoverflowerrorに遭遇するまで、非常にうまく機能しました。これには何の助けもありませんが、幸いなことに、以下のスケジューラ定義で動作し、他のすべてのテストもパスしました。

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

Starkej2の答えのように休んでください。これが誰かを助けることを願っています。

1
AA_PV

私はこの問題を抱えてこの投稿に来ましたが、RX 1については何も見つかりませんでした。したがって、これは最初のバージョンでも同じ問題がある場合の解決策です。

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

Kotlinを使用し、companion objectを作成する代わりにRuleを使用している場合は、@get:Ruleを使用できます。

したがって、使用する代わりに:

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

単に使用できます:

@get:Rule
val schedulers = RxImmediateSchedulerRule()
0
Skizo-ozᴉʞS