web-dev-qa-db-ja.com

非同期RxJavaコードのテスト-Android

私はRxJavaを知る過程にあります。私は自分の個人用アプリの1つでそれを使い始めて、コードの単体テストをしたいのですが、いくつかの問題があり、助けを求めています。

シナリオは簡単です。

  1. RESTを呼び出すことでUserInfoオブジェクトを取得します
  2. 再調整されたUserInfoオブジェクトがnullでない場合はtrueを返し、それ以外の場合はfalseを返します。

上記のシナリオでは、RxJavaコードは次のようになります。

public LiveData<Boolean> doesUserExists(String userName) {
    UserExistsObserver observer= new UserExistsObserver ();
    getUserInfo(userName).subscribeWith(subscriber);
    disposable.add(observer);
    return userExists;
}

public Observable<Boolean> getUserInfo(String userName) {
    return repository.getUserInfo(userName)
            .flatMap(new Function<UserInfo, Observable<Boolean>>() {
                @Override
                public Observable<Boolean> apply(UserInfo userInfo) throws Exception {
                      return Observable.just(userInfo != null);
                }
            })
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread());
}

そのため、getUserInfo()が正しいブール値を返すかどうかを確認する簡単な単体テストを作成したいと思いました。以下は私のユニットテストです。

 @Test
public void getUserInfo_returns_true(){
    UserInfo userInfo = new UserInfo(); //Dummy data - non null userInfo object
    when(repository.getUserInfo("username")).thenReturn(Observable.just(userInfo));

    TestObserver<Boolean> testObserver = new TestObserver<>();
    //the flatMap operator should return true since userInfo is not null
    viewModel.getUserInfo("username").subscribeWith(testObserver); 
    testObserver.assertValue(true);
}

そして以下は私のログです

Java.lang.AssertionError: Expected: true (class: Boolean), Actual: [] (latch = 1, values = 0, errors = 0, completions = 0)

at io.reactivex.observers.BaseTestConsumer.fail(BaseTestConsumer.Java:163)
at io.reactivex.observers.BaseTestConsumer.assertValue(BaseTestConsumer.Java:328)
at com.ik.githubbrowser.search_user.SearchUserViewModelTest.getUserInfo_returns_true(SearchUserViewModelTest.Java:51)
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)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.Java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.Java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.Java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.Java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.Java:26)
at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.Java:27)
at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.Java:55)
at org.junit.rules.RunRules.evaluate(RunRules.Java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.Java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.Java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.Java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.Java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.Java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.Java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.Java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.Java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.Java:363)
at org.mockito.internal.runners.JUnit45AndHigherRunnerImpl.run(JUnit45AndHigherRunnerImpl.Java:37)
at org.mockito.runners.MockitoJUnitRunner.run(MockitoJUnitRunner.Java:62)
at org.junit.runner.JUnitCore.run(JUnitCore.Java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.Java:117)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.Java:42)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.Java:262)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.Java:84)
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)
at com.intellij.rt.execution.application.AppMain.main(AppMain.Java:147)

Java.lang.NullPointerException
at io.reactivex.Android.schedulers.HandlerScheduler$HandlerWorker.schedule(HandlerScheduler.Java:70)
at io.reactivex.Scheduler$Worker.schedule(Scheduler.Java:272)
at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.schedule(ObservableObserveOn.Java:161)
at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.onNext(ObservableObserveOn.Java:119)
at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeOnObserver.onNext(ObservableSubscribeOn.Java:58)
at io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarDisposable.run(ObservableScalarXMap.Java:248)
at io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarXMapObservable.subscribeActual(ObservableScalarXMap.Java:164)
at io.reactivex.Observable.subscribe(Observable.Java:10903)
at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeTask.run(ObservableSubscribeOn.Java:96)
at io.reactivex.Scheduler$DisposeTask.run(Scheduler.Java:452)
at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.Java:61)
at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.Java:52)
at Java.util.concurrent.FutureTask.run(FutureTask.Java:266)
at Java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.Java:180)
at Java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.Java:293)
at Java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.Java:1142)
at Java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.Java:617)
at Java.lang.Thread.run(Thread.Java:745)

Exception in thread "RxCachedThreadScheduler-1" Java.lang.NullPointerException
at io.reactivex.Android.schedulers.HandlerScheduler$HandlerWorker.schedule(HandlerScheduler.Java:70)
at io.reactivex.Scheduler$Worker.schedule(Scheduler.Java:272)
at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.schedule(ObservableObserveOn.Java:161)
at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.onNext(ObservableObserveOn.Java:119)
at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeOnObserver.onNext(ObservableSubscribeOn.Java:58)
at io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarDisposable.run(ObservableScalarXMap.Java:248)
at io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarXMapObservable.subscribeActual(ObservableScalarXMap.Java:164)
at io.reactivex.Observable.subscribe(Observable.Java:10903)
at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeTask.run(ObservableSubscribeOn.Java:96)
at io.reactivex.Scheduler$DisposeTask.run(Scheduler.Java:452)
at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.Java:61)
at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.Java:52)
at Java.util.concurrent.FutureTask.run(FutureTask.Java:266)
at Java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.Java:180)
at Java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.Java:293)
at Java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.Java:1142)
at Java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.Java:617)
at Java.lang.Thread.run(Thread.Java:745)

Process finished with exit code -1
10
ik024

メソッドは、結果を生成するために2つの異なるスレッドを経由する必要があります(subscribeOnおよびobserveOn呼び出しのため)。これは、オブザーバーが実際に結果を生成するために時間が必要であることを意味します。オブザーバブルが実際に値を生成したことを確認するには、assertValueをチェックする前にTestObserver.awaitTerminalEvent()を使用します。

または、Androidスケジューラーがテスト環境で正しく機能するために追加のコードが必要になる場合があるため、コードをテストするときは異なるスケジューラーを使用する必要があります。

12
Kiskae

@kiskaeが示唆したように、スケジューラを置き換える必要がありました。 subscribeOnobserveOnの両方のスケジューラーを置き換えました。アイデアは、同じスレッド上で操作を実行して、同期させることです。この単体テストはJVMで実行されるため、JVMはobserveOnにスケジューラーとして渡されるAndroid特定のAndroidSchedulers.mainThread()_にアクセスできません。したがって、このスケジューラーはRxAndroidPluginsクラスの助けを借りてsubscribeOnクラスを使用してRxJavaPluginsに渡されたスケジューラーを置き換えるために同じことを行います。

詳細については this 中程度の投稿を参照してください。

以下は私の作業コードです。

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
import org.mockito.runners.MockitoJUnitRunner;

import Java.util.concurrent.Callable;

import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.Android.plugins.RxAndroidPlugins;
import io.reactivex.annotations.NonNull;
import io.reactivex.functions.Function;
import io.reactivex.observers.TestObserver;
import io.reactivex.plugins.RxJavaPlugins;
import io.reactivex.schedulers.Schedulers;

import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class) 
public class SearchUserViewModelTest {

    private RepositoryImpl repository;
    private SearchUserViewModel viewModel;

@BeforeClass
public static void before(){
    RxAndroidPlugins.reset();
    RxJavaPlugins.reset();
    RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() {
        @Override
        public Scheduler apply(@NonNull Scheduler scheduler) throws Exception {
            return Schedulers.trampoline();
        }
    });
    RxAndroidPlugins.setInitMainThreadSchedulerHandler(new Function<Callable<Scheduler>, Scheduler>() {
        @Override
        public Scheduler apply(@NonNull Callable<Scheduler> schedulerCallable) throws Exception {
            return Schedulers.trampoline();
        }
    });
}


@Before
public void setup(){

    MockitoAnnotations.initMocks(this);
    repository = mock(RepositoryImpl.class);
    viewModel = new SearchUserViewModel(repository);
}

@Test
public void getUserInfo_returns_true(){
    UserInfo userInfo = new UserInfo();
    userInfo.setName("");
    when(repository.getUserInfo(anyString())).thenReturn(Observable.just(userInfo));
    TestObserver<Boolean> testObserver = new TestObserver<>();
    viewModel.getUserInfo(anyString()).subscribe(testObserver);
    testObserver.assertValue(true);
}

@AfterClass
public static void after(){
    RxAndroidPlugins.reset();
    RxJavaPlugins.reset();
}

}
13
ik024