Android单元测试:测试RxJava的同步及异步操作
简述
在您开发的项目中,您使用了RxJava+Retrofit的网络请求框架,在RxJava强大的操作符下,您理所当然的认为您已经能够游刃有余地处理Android客户端开发中的联网请求需求,比如这样:
//Model层的网络请求public class HomeModel extends BaseModel implements HomeContract.Model { @Override public Maybe requestUserInfo(final String userName) { return serviceManager.getUserInfoService() .getUserInfo(userName) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); }}//Presenter层的网络处理public class HomePresenter extends BasePresenter implements HomeContract.Presenter { @Inject public HomePresenter(HomeContract.View rootView, HomeContract.Model model) { super(rootView, model); } @SuppressLint("VisibleForTests") @Override public void requestUserInfo(String userName) { mModel.requestUserInfo(userName) .subscribe(info -> Optional.ofNullable(info) .map(UserInfo::getLogin) .ifPresentOrElse(image -> mRootView.onGetUserInfo(info) , () -> mRootView.onError("用户信息为空")) , e -> mRootView.onError("请求出现异常")); }}
这是一个非常简单的MVP开发模式下的一个联网请求代码,用于请求用户的信息数据,我们不需要强制性理解每一行代码,我们需要关注的问题是,我们如何保证这些代码的正确性,即使在一些边界条件的限制下?
单元测试能够让项目中自己的代码更加健壮,但是无论是RxJava+Retrofit,亦或是okHttp3,异步操作的测试都是一个较高的门槛,加上目前国内开发环境的恶劣环境(周期短,需求变动反复),国内的相关技术文档相当匮乏。
Espresso的异步测试
笔者曾经花费一些时间研究Espresso的异步测试,并根据google官方的demo尝试总结在设备上进行测试异步操作的方案:
Android 自动化测试 Espresso篇:异步代码测试
这是可行的,但是有一些问题需要注意:
1.Espresso本质是依赖设备所进行的集成测试,这种测试是业务级的,测试的内容是一个整体的功能,相对于代码(方法)级别的单元测试,覆盖范围太大。
2.Espresso的测试依赖设备,所以进行一次完整的测试所消耗的时间更长。
3.Espresso更擅长UI的测试,而对于业务代码来说,Espresso并不擅长。
现在我们需要一种新的方式去测试业务代码,比如Junit+Mockito.
异步测试的难题
在尝试RxJava的单元测试时,不可避免的,你会遇到两个问题:
1.如何测试异步代码?
2.如何测试AndroidSchedulers.mainThread()?
1.异步操作如何转为同步进行单元测试?
前者的问题我们已经在之前的Android 自动化测试 Espresso篇:异步代码测试这篇文章中提到过了,那就是:
在我们还没有获取到数据进行验证时,测试代码已经跑完了。
以下面一段代码为例:
//开启一个子线程,每隔一秒发送一个数据,共发射10次,结果输出 0~9@Testfun test() {Observable.intervalRange(0, 10, 0, 1, TimeUnit.SECONDS) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) .subscribe(i -> System.out.println(i))}
理所当然的认为,我们能看到输出台上输出0~9,事实上,数据还没有输出一个,测试代码就已经跑完了,所以结果是,输出台空空如也。
单纯的使用Thread.sleep(10000)使当前的线程停留10秒不一定是最简单,但一定是最差的选择,因为这意味着我们需要花费更长时间去等待测试结果,更重要的问题是,并非每次我们都能直到异步操作的所需要的准确时间。
RxJavaPlugins
很快我找到了这篇文章:
Rxjava2单元测试的异步和同步转换
这篇文章是为数不多对我测试RxJava有用的文章之一,作者使用RxJavaPlugins,通过将RxJava所使用的线程换成Schedulers.trampoline()的方式,强制立即进行当前的任务,从而得到单元测试的结果。
代码如下:
/** * 单元测试的时候,利用RxJavaPlugins将io线程转换为trampoline * trampoline应该是立即执行的意思(待商榷),替代了Rx1的immediate。 */ public static void asyncToSync() { RxJavaPlugins.reset(); RxJavaPlugins.setIoSchedulerHandler(new Function() { @Override public Scheduler apply(Scheduler scheduler) throws Exception { return Schedulers.trampoline(); } });
然后再在测试类里加这么一句:
@Before public void setUp(){ //将rx异步转同步 RxjavaFactory.asyncToSync(); }
令我振奋的是,我成功得到了预期的结果。
TestScheduler
我们通过上面的代码,解决了一大部分的异步测试需求,但是还有一个问题,那就是测试的时间,我们不希望这个测试花费这么长时间。
RxJava2的开发人员已经为我们考虑到了这个问题,对此他们提供了TestScheduler类,很明显,这也是一个Scheduler.
我们定义一个Rule,用来配置TestScheduler代替我们所使用的其他Scheduler,这是一个特殊的scheduler,它允许我们手动的将一个虚拟时间提前:
class RxSchedulerRule : TestRule { private var testScheduler: TestScheduler = TestScheduler() override fun apply(base: Statement?, description: Description?): Statement { return object : Statement() { @Throws(Throwable::class) override fun evaluate() { RxJavaPlugins.setIoSchedulerHandler { testScheduler } RxJavaPlugins.setComputationSchedulerHandler { testScheduler } RxJavaPlugins.setNewThreadSchedulerHandler { testScheduler } RxJavaPlugins.setSingleSchedulerHandler { testScheduler }// RxAndroidPlugins.setInitMainThreadSchedulerHandler { testScheduler } try { base?.evaluate() } finally { RxJavaPlugins.reset()// RxAndroidPlugins.reset() } } } } //advanceTimeTo()以绝对的方式调整时间 fun advanceTimeTo(delayTime: Long, timeUnit: TimeUnit) { testScheduler.advanceTimeTo(delayTime, timeUnit) } //advanceTimeBy()方法将调度器的时间调整为相对于当前位置的时间 fun advanceTimeBy(delayTime: Long, timeUnit: TimeUnit) { testScheduler.advanceTimeBy(delayTime, timeUnit) } fun getScheduler(): TestScheduler { return testScheduler }}
对此,我们在每个测试类中添加这个Rule即可:
@Rule @JvmField val rxRule = RxSchedulerRule()
在Kotlin的测试代码中,我们需要添加@JvmField注释,因为@Rule注释仅适用于字段和getter方法,但rxRule 是Kotlin中的一个属性。
现在 我们再次进行测试:
class RxSchedulerRule_Test { @Rule @JvmField val rxRule = RxSchedulerRule() @Test fun testAdvanceTo_complete() { val observer = startRangeLoopThread() //将时间轴跳到第10秒,模拟执行发生的事件 rxRule.advanceTimeTo(10, TimeUnit.SECONDS) assertComplete(observer) } private fun startRangeLoopThread(): TestObserver { val observer = TestObserver() val observable = getThread() observable.subscribe(observer) return observer } //开启一个子线程,每隔一秒发送一个数据,共发射10次,结果 0~9 private fun getThread(): Observable { return Observable.intervalRange(0, 10, 0, 1, TimeUnit.SECONDS) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.newThread()) } //断言 private fun assertComplete(observer: TestObserver) { observer.assertValueCount(10) .assertComplete() .assertNoErrors() .assertValueAt(0, 0) .assertValueAt(9, 9) }}
这样我们仅仅需要数十或者数百毫秒,就能准确测试这个异步操作,并且验证我们预期的结果。
如何测试AndroidSchedulers.mainThread()
单纯的直接测试会导致报错:找不到AndroidSchedulers这个类。
我们将RxSchedulerRule 中的两行注解代码打开,这意味着当我们要使用AndroidSchedulers这个类时,RxAndroidPlugins已经用TestScheduler替换了AndroidSchedulers.mainThread()。
这样就解决了这个问题。
但是我很快遇到了一个奇怪的现象,那就是
当我一次运行整个测试类时,只有第一个测试通过,其余的测试通常会失败。
这个现象困扰我很久,最终我找到的解决方式是,通过依赖注入解决问题:
参考 【译】使用Kotlin和RxJava测试MVP架构的完整示例 - 第2部分 @ditclear
首先要做到这一点,我们需要创建一个SchedulerProvider接口,并提供两个实现。
- AppSchedulerProvider - 这将为我们提供真正的调度器。 我们将把这个类注入所有的presenter,这将为我们的Rx订阅提供调度器。
- TestSchedulerProvide - 这个类将为我们提供一个TestScheduler而不是真正的scheduler。 当我们在测试中实例化我们的presenter时,我们将使用它作为它的构造函数参数。
interface SchedulerProvider { fun uiScheduler() : Scheduler fun ioScheduler() : Scheduler}class AppSchedulerProvider : SchedulerProvider { override fun ioScheduler() = Schedulers.io() override fun uiScheduler(): Scheduler = AndroidSchedulers.mainThread()}class TestSchedulerProvider() : SchedulerProvider { val testScheduler: TestScheduler = TestScheduler() override fun uiScheduler() = testScheduler override fun ioScheduler() = testScheduler}
通过依赖注入,在生产代码中使用AppSchedulerProvider ,在测试代码中使用TestSchedulerProvider。
以最初的MVP中的model和Presenter为例,测试代码如下:
- Model
class HomeModelTest : BaseTestModel() { private var retrofit: MockRetrofit = MockRetrofit() private var homeModel: HomeModel = HomeModel(mock()) val provider = TestSchedulerProvider() @Before fun setUp() { homeModel.schedulers = provider homeModel = spy(homeModel) val service = retrofit.create(UserInfoService::class.java) whenever(homeModel.serviceManager.userInfoService).thenReturn(service) } @Test fun requestUserInfo() { val testObserver = TestObserver() retrofit.path = MockAssest.USER_DATA doReturn(RxTestTransformer()).whenever(homeModel).getUserInfoCache(anyString(), anyBoolean()) val maybe = homeModel.requestUserInfo("qingmei2") maybe.subscribe(testObserver) provider.testScheduler.triggerActions() testObserver.assertValue { it -> it.login == "login" && it.name == "name" } }}
- Presenter
class HomePresenterTest : BaseTestPresenter() { val view: HomeContract.View = mock() val model: HomeContract.Model = mock() var presenter: HomePresenter = HomePresenter(view, model) @Before fun setUp() { presenter = spy(presenter) doReturn(RxTestTransformer()).whenever(presenter).bindViewMaybe(view) } @Test fun requestUserInfo_success() { val s = MockAssest.readFile(MockAssest.USER_DATA) val user = Gson().fromJson(s, UserInfo::class.java) whenever(model.requestUserInfo(anyString())).thenReturn(Maybe.just(user)) //testing presenter.requestUserInfo("qingmei2") val captor: ArgumentCaptor = ArgumentCaptor.forClass(UserInfo::class.java) verify(view).onGetUserInfo(captor.capture()); verify(view, never()).onError(anyString()); assert(user.equals(captor.value)) } @Test fun requestUserInfo_failed_error() { val s = MockAssest.readFile(MockAssest.error) val user = Gson().fromJson(s, UserInfo::class.java) whenever(model.requestUserInfo(anyString())).thenReturn(Maybe.just(user)) presenter.requestUserInfo("qingmei2") verify(view, never()).onGetUserInfo(any()); verify(view).onError("用户信息为空"); }}
小结
文笔所限,只是把思路整理一遍写了出来,很多类都没有详细去讲解,比如TestScheduler,比如TestObserver,这都是RxJava提供好的工具,我们一定要好好利用。
虽然思路有些天马行空,但是笔者最近数日整理出来的一套Kotlin的RxJava2+Retrofit的单元测试脚手架已经搭建完毕,并且作为笔者个人MVP架构下的测试框架,配以详细的单元测试代码,上传到了Github上,有需要的朋友可以参考:
MvpArchitecture-Android
参考文章
测试RxJava2
http://www.infoq.com/cn/articles/Testing-RxJava2
【译】使用Kotlin和RxJava测试MVP架构的完整示例 - 第2部分:
http://www.jianshu.com/p/0a845ae2ca64
更多相关文章
- Kotlin Anko 使用相对布局 RelativeLayout
- Android仿网易新闻图片详情下滑隐藏效果示例代码
- android解决:使用多线程和Handler同步更新UI
- Android(安卓)退出所有Activity
- Android培训班(26)
- Android(安卓)Intent的几种用法全面总结 --
- android播放网络或者本地视频(截取某帧做封面)VideoView原生控件
- Android邪门歪道
- Android(安卓)API之ImageView.ScaleType代码演示