web-dev-qa-db-ja.com

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

RxJavaでレトロフィットを使用しているAndroidアプリを開発しました。現在、Mockitoで単体テストを設定しようとしていますが、APIレスポンスを順番にモックする方法がわかりません。実際の呼び出しは行わないが、偽の応答を行うテストを作成します。

たとえば、splashPresenterでsyncGenresメソッドが正常に機能していることをテストします。私のクラスは次のとおりです。

public class SplashPresenterImpl implements SplashPresenter {

private SplashView splashView;

public SplashPresenterImpl(SplashView splashView) {
    this.splashView = splashView;
}

@Override
public void syncGenres() {
    Api.syncGenres(new Subscriber<List<Genre>>() {
        @Override
        public void onError(Throwable e) {
            if(splashView != null) {
                splashView.onError();
            }
        }

        @Override
        public void onNext(List<Genre> genres) {
            SharedPreferencesUtils.setGenres(genres);
            if(splashView != null) {
                splashView.navigateToHome();
            }
        }
    });
}
}

apiクラスは次のようなものです:

public class Api {
    ...
    public static Subscription syncGenres(Subscriber<List<Genre>> apiSubscriber) {
        final Observable<List<Genre>> call = ApiClient.getService().syncGenres();
        return call
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(apiSubscriber);
    }

}

今、SplashPresenterImplクラスをテストしようとしていますが、それを行う方法がわかりません。次のようなことをする必要があります。

public class SplashPresenterImplTest {

@Mock
Api api;
@Mock
private SplashView splashView;

@Captor
private ArgumentCaptor<Callback<List<Genre>>> cb;

private SplashPresenterImpl splashPresenter;

@Before
public void setupSplashPresenterTest() {
    // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To
    // inject the mocks in the test the initMocks method needs to be called.
    MockitoAnnotations.initMocks(this);

    // Get a reference to the class under test
    splashPresenter = new SplashPresenterImpl(splashView);
}

@Test
public void syncGenres_success() {

    Mockito.when(api.syncGenres(Mockito.any(ApiSubscriber.class))).thenReturn(); // I don't know how to do that

    splashPresenter.syncGenres();
    Mockito.verify(api).syncGenres(Mockito.any(ApiSubscriber.class)); // I don't know how to do that



}
}

APIの応答をどのようにモックし、検証する必要があるかについてのアイデアはありますか?前もって感謝します!

編集:@invariantの提案に従って、クライアントオブジェクトをプレゼンターに渡し、そのapiはサブスクリプションではなくObservableを返します。ただし、API呼び出しを実行すると、サブスクライバーでNullPointerExceptionが発生します。テストクラスは次のようになります。

public class SplashPresenterImplTest {
@Mock
Api api;
@Mock
private SplashView splashView;

private SplashPresenterImpl splashPresenter;

@Before
public void setupSplashPresenterTest() {
    // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To
    // inject the mocks in the test the initMocks method needs to be called.
    MockitoAnnotations.initMocks(this);

    // Get a reference to the class under test
    splashPresenter = new SplashPresenterImpl(splashView, api);
}

@Test
public void syncGenres_success() {
    Mockito.when(api.syncGenres()).thenReturn(Observable.just(Collections.<Genre>emptyList()));


    splashPresenter.syncGenres();


    Mockito.verify(splashView).navigateToHome();
}
}

NullPointerExceptionが発生するのはなぜですか?

どうもありがとう!

16
FVod

RxJavaとRetrofitをテストする方法

1.静的呼び出しを取り除きます-依存性注入を使用します

コードの最初の問題は、静的メソッドを使用することです。これは、実装のモックを作成するのが難しくなるため、少なくとも簡単にテストできるアーキテクチャではありません。適切に行うには、ApiClient.getService()にアクセスするApiを使用する代わりに、コンストラクターを介してプレゼンターにこのserviceを注入します。

_public class SplashPresenterImpl implements SplashPresenter {

private SplashView splashView;
private final ApiService service;

public SplashPresenterImpl(SplashView splashView, ApiService service) {
    this.splashView = splashView;
    this.apiService = service;
}
_

2.テストクラスを作成する

JUnitテストクラスを実装し、プレゼンターを_@Before_メソッドのモック依存関係で初期化します。

_public class SplashPresenterImplTest {

@Mock
ApiService apiService;

@Mock
SplashView splashView;

private SplashPresenter splashPresenter;

@Before
public void setUp() throws Exception {
    this.splashPresenter = new SplashPresenter(splashView, apiService);
}
_

3.モックとテスト

次に、実際のモックとテストが行​​われます。例:

_@Test
public void testEmptyListResponse() throws Exception {
    // given
    when(apiService.syncGenres()).thenReturn(Observable.just(Collections.emptyList());
    // when
    splashPresenter.syncGenres();
    // then
    verify(... // for example:, verify call to splashView.navigateToHome()
}
_

そうすれば、Observable + Subscriptionをテストできます。Observableが正しく動作するかどうかをテストする場合は、TestSubscriberのインスタンスでサブスクライブします。


トラブルシューティング

Schedulers.io()AndroidSchedulers.mainThread()などのRxJavaおよびRxAndroidスケジューラでテストする場合、オブザーバブル/サブスクリプションテストの実行でいくつかの問題が発生する可能性があります。

NullPointerException

最初は、与えられたスケジューラーを適用する行にスローされるNullPointerExceptionです。例えば:

_.observeOn(AndroidSchedulers.mainThread()) // throws NPE
_

原因は、AndroidSchedulers.mainThread()が内部的にAndroidのLooperSchedulerスレッドを使用するLooperであるためです。この依存関係はJUnitテスト環境では利用できないため、呼び出しの結果はNullPointerExceptionになります。

競合状態

2番目の問題は、適用されたスケジューラーが別のワーカースレッドを使用してobservableを実行する場合、_@Test_メソッドを実行するスレッドとそのワーカースレッドの間で競合状態が発生することです。通常、観察可能な実行が終了する前にテストメソッドが返されます。

解決策

上記の問題は両方とも、テストに準拠したスケジューラを提供することで簡単に解決でき、いくつかのオプションがあります。

  1. RxJavaHooksおよびRxAndroidPlugins AP​​Iを使用して、_Schedulers.?_および_AndroidSchedulers.?_への呼び出しをオーバーライドし、ObservableにScheduler.immediate()などを使用するように強制します。

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

    このコードはObservableテストをラップする必要があるため、示されているように_@Before_および_@After_内で実行でき、JUnit _@Rule_に配置するか、コードの任意の場所に配置できます。フックをリセットすることを忘れないでください。

  2. 2番目のオプションは、依存性注入を通じて明示的なSchedulerインスタンスをクラス(プレゼンター、DAO)に提供し、再びSchedulers.immediate()(またはテストに適した他の)を使用することです。

  3. @aleienが指摘したように、RxTransformerアプリケーションを実行するSchedulerインスタンスを挿入することもできます。

私は最初の方法を使用して、生産で良い結果を得ました。

29
maciekjanusz

syncGenresメソッドがObservableの代わりにSubscriptionを返すようにします。次に、実際のAPI呼び出しを行う代わりに、このメソッドをモックしてObservable.just(...)を返します。

Subscriptionをそのメソッドの戻り値として保持したい場合(Observableの構成可能性を損なうのでお勧めしません)、このメソッドを静的ではなく、渡す必要があります。 ApiClient.getService()はコンストラクタパラメーターとして返され、テストでモックされたサービスオブジェクトを使用します(この手法は依存性注入と呼ばれます)

2
krp

APIメソッドからSubscriptionを返す特別な理由はありますか?通常、apiメソッドからObservable(またはSingle)を返す方が便利です(特に、Retrofitが呼び出しの代わりにObservablesとSinglesを生成できることに関して)。特別な理由がない場合は、次のようなものに切り替えることをお勧めします。

public interface Api {
    @GET("genres")
    Single<List<Genre>> syncGenres();
    ...
}

したがって、apiへの呼び出しは次のようになります。

...
Api api = retrofit.create(Api.class);
api.syncGenres()
   .subscribeOn(Schedulers.io())
   .observeOn(AndroidSheculers.mainThread())
   .subscribe(genres -> soStuff());

そのようにして、APIクラスをモックして次のように書くことができます。

List<Genre> mockedGenres = Arrays.asList(genre1, genre2...);
Mockito.when(api.syncGenres()).thenReturn(Single.just(mockedGenres));

また、テストはワーカースレッドを待機しないため、ワーカースレッドで応答をテストできないことを考慮する必要があります。この問題を回避するには recommendreadingthesearticles そして、scheduler managerまたは などの使用を検討してくださいトランスフォーマー プレゼンターに使用するスケジューラー(実際のスケジューラーまたはテストスケジューラー)を明示的に指示できるようにする

1
aleien

私は同じ問題を抱えていました

.observeOn(AndroidSchedulers.mainThread())

次のコードで修正しました

public class RxJavaUtils {
    public static Supplier<Scheduler> getSubscriberOn = () -> Schedulers.io();
    public static Supplier<Scheduler> getObserveOn = () -> AndroidSchedulers.mainThread();
}

そして、このように使用します

deviceService.findDeviceByCode(text)
            .subscribeOn(RxJavaUtils.getSubscriberOn.get())
            .observeOn(RxJavaUtils.getObserveOn.get())

そして私のテストでは

@Before
public void init(){
    getSubscriberOn = () -> Schedulers.from(command -> command.run()); //Runs in curren thread
    getObserveOn = () -> Schedulers.from(command -> command.run()); //runs also in current thread
}

io.reactivexでも動作します

0
Simon Ludwig

@maciekjanuszの解決策は説明と一緒に完璧なので、私はこれを言うだけです、Schedulers.io()AndroidSchedulers.mainThread()を使用すると問題が発生します。 @maciekjanuszの答えの問題は、理解するのが複雑すぎて、だれもがDagger2を使用しているわけではないということです(そうするべきです)。また、確信はありませんが、RxJava2ではRxJavaHooksのインポートが機能しませんでした。

RxJava2のより良いソリューション:-

RxSchedulersOverrideRule をテストパッケージに追加し、テストクラスに次の行を追加します。

@Rule
public RxSchedulersOverrideRule schedulersOverrideRule = new RxSchedulersOverrideRule();

これで、追加することは何もありません。テストケースは正常に実行されるはずです。

0
Rohan Kandwal

私はこれらのクラスを使用します:

  1. サービス
  2. RemoteDataSource
  3. RemoteDataSourceTest
  4. トピックプレゼンター
  5. TopicPresenterTest

シンプルなサービス:

public interface Service {
    String URL_BASE = "https://guessthebeach.herokuapp.com/api/";

    @GET("topics/")
    Observable<List<Topics>> getTopicsRx();

}

RemoteDataSourceの場合

public class RemoteDataSource implements Service {

    private Service api;

    public RemoteDataSource(Retrofit retrofit) {


        this.api = retrofit.create(Service.class);
    }


    @Override
    public Observable<List<Topics>> getTopicsRx() {
        return api.getTopicsRx();
    }
}

キーは okhttp3のMockWebServer です。

このライブラリを使用すると、HTTPおよびHTTPS呼び出しを行うときにアプリが適切に動作することを簡単にテストできます。返される応答を指定し、要求が期待どおりに行われたことを確認できます。

完全なHTTPスタックを実行するため、すべてをテストしていると確信できます。実際のWebサーバーからHTTP応答をコピーして貼り付け、代表的なテストケースを作成することもできます。または、500エラーや読み込みの遅い応答など、再現しにくい状況でコードが生き残ることをテストします。

Mockitoのようなモックフレームワークを使用するのと同じ方法でMockWebServerを使用します。

モックのスクリプトを作成します。アプリケーションコードを実行します。予想される要求が行われたことを確認します。 RemoteDataSourceTestの完全な例を次に示します。

public class RemoteDataSourceTest {

    List<Topics> mResultList;
    MockWebServer mMockWebServer;
    TestSubscriber<List<Topics>> mSubscriber;

    @Before
    public void setUp() {
        Topics topics = new Topics(1, "Discern The Beach");
        Topics topicsTwo = new Topics(2, "Discern The Football Player");
        mResultList = new ArrayList();
        mResultList.add(topics);
        mResultList.add(topicsTwo);

        mMockWebServer = new MockWebServer();
        mSubscriber = new TestSubscriber<>();
    }

    @Test
    public void serverCallWithError() {
        //Given
        String url = "dfdf/";
        mMockWebServer.enqueue(new MockResponse().setBody(new Gson().toJson(mResultList)));
        Retrofit retrofit = new Retrofit.Builder()
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .baseUrl(mMockWebServer.url(url))
                .build();
        RemoteDataSource remoteDataSource = new RemoteDataSource(retrofit);

        //When
        remoteDataSource.getTopicsRx().subscribe(mSubscriber);

        //Then
        mSubscriber.assertNoErrors();
        mSubscriber.assertCompleted();
    }

    @Test
    public void severCallWithSuccessful() {
        //Given
        String url = "https://guessthebeach.herokuapp.com/api/";
        mMockWebServer.enqueue(new MockResponse().setBody(new Gson().toJson(mResultList)));
        Retrofit retrofit = new Retrofit.Builder()
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .baseUrl(mMockWebServer.url(url))
                .build();
        RemoteDataSource remoteDataSource = new RemoteDataSource(retrofit);

        //When
        remoteDataSource.getTopicsRx().subscribe(mSubscriber);

        //Then
        mSubscriber.assertNoErrors();
        mSubscriber.assertCompleted();
    }

}

GitHub および このチュートリアル で私の例を確認できます。

また、プレゼンターでは、RxJavaを使用したサーバー呼び出しを確認できます。

public class TopicPresenter implements TopicContract.Presenter {

    @NonNull
    private TopicContract.View mView;

    @NonNull
    private BaseSchedulerProvider mSchedulerProvider;

    @NonNull
    private CompositeSubscription mSubscriptions;

    @NonNull
    private RemoteDataSource mRemoteDataSource;


    public TopicPresenter(@NonNull RemoteDataSource remoteDataSource, @NonNull TopicContract.View view, @NonNull BaseSchedulerProvider provider) {
        this.mRemoteDataSource = checkNotNull(remoteDataSource, "remoteDataSource");
        this.mView = checkNotNull(view, "view cannot be null!");
        this.mSchedulerProvider = checkNotNull(provider, "schedulerProvider cannot be null");

        mSubscriptions = new CompositeSubscription();

        mView.setPresenter(this);
    }

    @Override
    public void fetch() {

        Subscription subscription = mRemoteDataSource.getTopicsRx()
                .subscribeOn(mSchedulerProvider.computation())
                .observeOn(mSchedulerProvider.ui())
                .subscribe((List<Topics> listTopics) -> {
                            mView.setLoadingIndicator(false);
                            mView.showTopics(listTopics);
                        },
                        (Throwable error) -> {
                            try {
                                mView.showError();
                            } catch (Throwable t) {
                                throw new IllegalThreadStateException();
                            }

                        },
                        () -> {
                        });

        mSubscriptions.add(subscription);
    }

    @Override
    public void subscribe() {
        fetch();
    }

    @Override
    public void unSubscribe() {
        mSubscriptions.clear();
    }

}

そして今、TopicPresenterTest:

@RunWith(MockitoJUnitRunner.class)
public class TopicPresenterTest {

    @Mock
    private RemoteDataSource mRemoteDataSource;

    @Mock
    private TopicContract.View mView;

    private BaseSchedulerProvider mSchedulerProvider;

    TopicPresenter mThemePresenter;

    List<Topics> mList;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);

        Topics topics = new Topics(1, "Discern The Beach");
        Topics topicsTwo = new Topics(2, "Discern The Football Player");
        mList = new ArrayList<>();
        mList.add(topics);
        mList.add(topicsTwo);

        mSchedulerProvider = new ImmediateSchedulerProvider();
        mThemePresenter = new TopicPresenter(mRemoteDataSource, mView, mSchedulerProvider);


    }

    @Test
    public void fetchData() {

        when(mRemoteDataSource.getTopicsRx())
                .thenReturn(rx.Observable.just(mList));

        mThemePresenter.fetch();

        InOrder inOrder = Mockito.inOrder(mView);
        inOrder.verify(mView).setLoadingIndicator(false);
        inOrder.verify(mView).showTopics(mList);

    }

    @Test
    public void fetchError() {

        when(mRemoteDataSource.getTopicsRx())
                .thenReturn(Observable.error(new Throwable("An error has occurred!")));
        mThemePresenter.fetch();

        InOrder inOrder = Mockito.inOrder(mView);
        inOrder.verify(mView).showError();
        verify(mView, never()).showTopics(anyList());
    }

}

GitHub および この記事 で私の例を確認できます。

0
Cabezas