使用改造和 rxjava 对 android 应用程序进行单元测试 [英] Unit testing android application with retrofit and rxjava
问题描述
我开发了一个使用 rxJava 改造的 android 应用程序,现在我正在尝试使用 Mockito 设置单元测试,但我不知道如何模拟 api 响应以创建不打真正的电话,但有虚假的回应.
I have developed an android app that is using retrofit with rxJava, and now I'm trying to set up the unit tests with Mockito but I don't know how to mock the api responses in order to create tests that do not do the real calls but have fake responses.
例如,我想测试方法 syncGenres 是否适用于我的 SplashPresenter.我的课程如下:
For instance, I want to test that the method syncGenres is working fine for my SplashPresenter. My classes are as follow:
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 类,但我不知道该怎么做,我应该这样做:
Now I'm trying to test the SplashPresenterImpl class but I don't know how to do that, I should do something like:
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 响应有任何想法吗?提前致谢!
Do you have any idea about how should I mock and verify the api responses? Thanks in advance!
遵循@invariant 的建议,现在我将一个客户端对象传递给我的演示者,并且该 api 返回一个 Observable 而不是订阅.但是,在进行 api 调用时,我的订阅服务器上出现 NullPointerException.测试类如下所示:
Following @invariant suggestion, now I'm passing a client object to my presenter, and that api returns an Observable instead of a Subscription. However, I'm getting a NullPointerException on my Subscriber when doing the api call. The test class looks like:
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?
Why am I getting that NullPointerException?
非常感谢!
推荐答案
如何测试 RxJava 和 Retrofit
1.摆脱静态调用——使用依赖注入
您的代码中的第一个问题是您使用静态方法.这不是一个可测试的架构,至少不容易,因为它使模拟实现变得更加困难.为了正确地做事,不要使用访问 ApiClient.getService()
的 Api
,而是通过构造函数将此 service 注入到 Presenter:
How to test RxJava and Retrofit
1. Get rid of the static call - use dependency injection
The first problem in your code is that you use static methods. This is not a testable architecture, at least not easily, because it makes it harder to mock the implementation.
To do things properly, instead of using Api
that accesses ApiClient.getService()
, inject this service to the presenter through the constructor:
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
方法中使用模拟依赖项初始化演示者:
2. Create the test class
Implement your JUnit test class and initialize the presenter with mock dependencies in the @Before
method:
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.模拟和测试
然后是实际的模拟和测试,例如:
3. Mock and test
Then comes the actual mocking and testing, for example:
@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
的实例订阅它.
That way you can test your Observable + Subscription, if you want to test if the Observable behaves correctly, subscribe to it with an instance of TestSubscriber
.
使用 RxJava 和 RxAndroid 调度程序进行测试时,例如 Schedulers.io()
和 AndroidSchedulers.mainThread()
,您可能会在运行 observable/subscription 测试时遇到一些问题.
When testing with RxJava and RxAndroid schedulers, such as Schedulers.io()
and AndroidSchedulers.mainThread()
you might encounter several problems with running your observable/subscription tests.
第一个是 NullPointerException
在应用给定调度程序的行上抛出,例如:
The first is NullPointerException
thrown on the line that applies given scheduler, for example:
.observeOn(AndroidSchedulers.mainThread()) // throws NPE
原因是 AndroidSchedulers.mainThread()
内部是一个 LooperScheduler
,它使用了 android 的 Looper
线程.这种依赖在 JUnit 测试环境中不可用,因此调用会导致 NullPointerException.
The cause is that AndroidSchedulers.mainThread()
is internally a LooperScheduler
that uses android's Looper
thread. This dependency is not available on JUnit test environment, and thus the call results in a NullPointerException.
第二个问题是,如果应用的调度器使用单独的工作线程来执行observable,那么执行@Test
方法的线程和该工作线程之间就会出现竞争条件.通常它会导致在可观察执行完成之前返回测试方法.
The second problem is that if applied scheduler uses a separate worker thread to execute observable, the race condition occurs between the thread that executes the @Test
method and the said worker thread. Usually it results in test method returning before observable execution finishes.
上述两个问题都可以通过提供符合测试标准的调度程序轻松解决,而且选择很少:
Both of the said problems can be easily solved by supplying test-compliant schedulers, and there are few options:
使用
RxJavaHooks
和RxAndroidPlugins
API 覆盖对Schedulers.?
和AndroidSchedulers.?
,强制 Observable 使用,例如Scheduler.immediate()
:
Use
RxJavaHooks
andRxAndroidPlugins
API to override any call toSchedulers.?
andAndroidSchedulers.?
, forcing the Observable to use, for example,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 @规则
或放置在代码中的任何位置.只是不要忘记重置挂钩.
This code has to wrap the Observable test, so it can be done within @Before
and @After
as shown, it can be put into JUnit @Rule
or placed anywhere in the code. Just don't forget to reset the hooks.
第二个选项是通过依赖注入向类(Presenters、DAO)提供显式的 Scheduler
实例,然后再次使用 Scheduler.immediate()
(或其他适合测试的).
Second option is to provide explicit Scheduler
instances to classes (Presenters, DAOs) through dependency injection, and again just use Schedulers.immediate()
(or other suitable for testing).
正如@aleien 所指出的,您还可以使用注入的 RxTransformer
实例来执行 Scheduler
应用程序.
As pointed out by @aleien, you can also use an injected RxTransformer
instance that executes Scheduler
application.
我在生产中使用了第一种方法,效果很好.
I've used the first method with good results in production.
这篇关于使用改造和 rxjava 对 android 应用程序进行单元测试的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!