用retrofit和rxjava对android应用程序进行单元测试



我开发了一个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方法的线程和该工作线程之间就会出现竞争条件。通常它会导致测试方法在可观察对象执行结束之前返回。

解决方案

上述两个问题都可以通过提供符合测试的调度器轻松解决,并且有几个选项:

  1. 使用RxJavaHooksRxAndroidPlugins 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或放在代码的任何地方。只是别忘了重置钩子。

  2. 第二种选择是通过依赖注入向类(presenter, dao)提供显式的Scheduler实例,同样只使用Schedulers.immediate()(或其他适合测试的)。

  3. 正如@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));

你还必须考虑到你将无法在工作线程上测试响应,因为测试不会等待它们。为了绕过这个问题,我建议阅读这些文章,并考虑使用调度器管理器或转换器之类的东西,以便能够显式地告诉演示者使用哪个调度器(真实的或测试的)

我使用这些类:

  1. RemoteDataSource
  2. RemoteDataSourceTest
  3. TopicPresenter
  4. 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

相关内容

  • 没有找到相关文章

最新更新