我开发了一个android应用程序,使用rxJava进行改造,现在我试图用Mockito设置单元测试,但我不知道如何模拟api响应,以便创建不做真实调用但有假响应的测试。
例如,我想测试syncgenre方法是否对我的SplashPresenter有效。我的课程如下:
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而不是Subscription。然而,我得到一个NullPointerException在我的订阅者做api调用时。测试类看起来像:
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?
非常感谢!
如何测试RxJava和Retrofit
1。摆脱静态调用-使用依赖注入
代码中的第一个问题是使用静态方法。这不是一个可测试的体系结构,至少不容易,因为它使模拟实现变得更加困难。为了正确地完成工作,不要使用访问ApiClient.getService()
的Api
,而是通过构造函数将服务注入到演示器:
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 +订阅,如果你想测试Observable的行为是否正确,用TestSubscriber
的实例订阅它。
<标题>故障排除h1> 使用RxJava和RxAndroid调度器(如
Schedulers.io()
和AndroidSchedulers.mainThread()
)进行测试时,您可能会在运行可观察/订阅测试时遇到几个问题。NullPointerException
第一个是在应用给定调度器的行上抛出的NullPointerException
,例如:
.observeOn(AndroidSchedulers.mainThread()) // throws NPE
原因是AndroidSchedulers.mainThread()
内部是一个LooperScheduler
,使用android的Looper
线程。此依赖项在JUnit测试环境中不可用,因此调用将导致NullPointerException。
竞态条件
第二个问题是,如果应用调度器使用一个单独的工作线程来执行可观察对象,那么执行@Test
方法的线程和该工作线程之间就会出现竞争条件。通常它会导致测试方法在可观察对象执行结束之前返回。
上述两个问题都可以通过提供符合测试的调度器轻松解决,并且有几个选项:
使用
RxJavaHooks
和RxAndroidPlugins
API来覆盖对Schedulers.?
和AndroidSchedulers.?
的任何调用,强制观察对象使用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
或放在代码的任何地方。只是别忘了重置钩子。第二种选择是通过依赖注入向类(presenter, dao)提供显式的
Scheduler
实例,同样只使用Schedulers.immediate()
(或其他适合测试的)。正如@aleien指出的,你也可以使用一个注入的
RxTransformer
实例来执行Scheduler
应用程序。
我在生产中使用了第一种方法,效果很好。
标题>让您的syncGenres
方法返回Observable
而不是Subscription
。然后,您可以模拟此方法以返回Observable.just(...)
,而不是进行真正的api调用。
如果你想保持Subscription
作为该方法的返回值(我不建议,因为它破坏了Observable
的可组合性),你需要使这个方法不是静态的,并传递任何ApiClient.getService()
作为构造函数参数返回,并在测试中使用模拟服务对象(这种技术称为依赖注入)
从api方法返回Subscription有什么特别的原因吗?从api方法返回Observable(或Single)通常更方便(特别是考虑到Retrofit能够生成Observables和Single而不是调用)。如果没有特殊的原因,我建议转换成这样:
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));
你还必须考虑到你将无法在工作线程上测试响应,因为测试不会等待它们。为了绕过这个问题,我建议阅读这些文章,并考虑使用调度器管理器或转换器之类的东西,以便能够显式地告诉演示者使用哪个调度器(真实的或测试的)
我使用这些类:
- RemoteDataSource
- RemoteDataSourceTest
- TopicPresenter
- 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();
}
}
key是MockWebServer from okhttp3.
这个库可以很容易地测试你的应用程序做正确的事情时,它进行HTTP和HTTPS调用。它允许您指定返回哪些响应,然后验证请求是否按预期发出。
因为它测试了完整的HTTP堆栈,所以您可以确信正在测试所有内容。你甚至可以复制&从真实的web服务器粘贴HTTP响应来创建有代表性的测试用例。或者测试你的代码是否能在诸如500个错误或缓慢加载响应等难以重现的情况下存活。
使用MockWebServer的方法与使用mock框架(如Mockito)的方法相同:
编写模拟脚本。运行应用程序代码。验证是否发出了预期的请求。下面是remotedatasourcest中的完整示例:
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和本文中查看我的示例。
解决方案@maciekjanusz是完美的解释,所以我只会这么说,当你使用Schedulers.io()
和AndroidSchedulers.mainThread()
时,问题就会发生。@maciekjanusz的答案的问题是,它太复杂了,难以理解,而且仍然不是每个人都使用Dagger2(他们应该使用Dagger2)。此外,我不太确定,但与RxJava2我的RxJavaHooks
的导入不工作。
RxJava2的更好解决方案:-
将RxSchedulersOverrideRule添加到测试包中,并在测试类中添加以下行。
@Rule
public RxSchedulersOverrideRule schedulersOverrideRule = new RxSchedulersOverrideRule();
就是这样,没有其他要添加的了,您的测试用例现在应该运行良好了。
我也有同样的问题
.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