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が発生するのはなぜですか?
どうもありがとう!
コードの最初の問題は、静的メソッドを使用することです。これは、実装のモックを作成するのが難しくなるため、少なくとも簡単にテストできるアーキテクチャではありません。適切に行うには、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;
}
_
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);
}
_
次に、実際のモックとテストが行われます。例:
_@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
です。例えば:
_.observeOn(AndroidSchedulers.mainThread()) // throws NPE
_
原因は、AndroidSchedulers.mainThread()
が内部的にAndroidのLooperScheduler
スレッドを使用するLooper
であるためです。この依存関係はJUnitテスト環境では利用できないため、呼び出しの結果はNullPointerExceptionになります。
2番目の問題は、適用されたスケジューラーが別のワーカースレッドを使用してobservableを実行する場合、_@Test
_メソッドを実行するスレッドとそのワーカースレッドの間で競合状態が発生することです。通常、観察可能な実行が終了する前にテストメソッドが返されます。
上記の問題は両方とも、テストに準拠したスケジューラを提供することで簡単に解決でき、いくつかのオプションがあります。
RxJavaHooks
およびRxAndroidPlugins
APIを使用して、_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番目のオプションは、依存性注入を通じて明示的なScheduler
インスタンスをクラス(プレゼンター、DAO)に提供し、再びSchedulers.immediate()
(またはテストに適した他の)を使用することです。
@aleienが指摘したように、RxTransformer
アプリケーションを実行するScheduler
インスタンスを挿入することもできます。
私は最初の方法を使用して、生産で良い結果を得ました。
syncGenres
メソッドがObservable
の代わりにSubscription
を返すようにします。次に、実際のAPI呼び出しを行う代わりに、このメソッドをモックしてObservable.just(...)
を返します。
Subscription
をそのメソッドの戻り値として保持したい場合(Observable
の構成可能性を損なうのでお勧めしません)、このメソッドを静的ではなく、渡す必要があります。 ApiClient.getService()
はコンストラクタパラメーターとして返され、テストでモックされたサービスオブジェクトを使用します(この手法は依存性注入と呼ばれます)
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または などの使用を検討してくださいトランスフォーマー プレゼンターに使用するスケジューラー(実際のスケジューラーまたはテストスケジューラー)を明示的に指示できるようにする
私は同じ問題を抱えていました
.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でも動作します
@maciekjanuszの解決策は説明と一緒に完璧なので、私はこれを言うだけです、Schedulers.io()
とAndroidSchedulers.mainThread()
を使用すると問題が発生します。 @maciekjanuszの答えの問題は、理解するのが複雑すぎて、だれもがDagger2を使用しているわけではないということです(そうするべきです)。また、確信はありませんが、RxJava2ではRxJavaHooks
のインポートが機能しませんでした。
RxJava2のより良いソリューション:-
RxSchedulersOverrideRule をテストパッケージに追加し、テストクラスに次の行を追加します。
@Rule
public RxSchedulersOverrideRule schedulersOverrideRule = new RxSchedulersOverrideRule();
これで、追加することは何もありません。テストケースは正常に実行されるはずです。
私はこれらのクラスを使用します:
シンプルなサービス:
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());
}
}