簡述
在您開發(fā)的項目中险毁,您使用了RxJava+Retrofit的網(wǎng)絡(luò)請求框架,在RxJava強(qiáng)大的操作符下钱反,您理所當(dāng)然的認(rèn)為您已經(jīng)能夠游刃有余地處理Android客戶端開發(fā)中的聯(lián)網(wǎng)請求需求僻焚,比如這樣:
//Model層的網(wǎng)絡(luò)請求
public class HomeModel extends BaseModel<ServiceManager> implements HomeContract.Model {
@Override
public Maybe<UserInfo> requestUserInfo(final String userName) {
return serviceManager.getUserInfoService()
.getUserInfo(userName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
}
//Presenter層的網(wǎng)絡(luò)處理
public class HomePresenter extends BasePresenter<HomeContract.View, HomeContract.Model> 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("請求出現(xiàn)異常"));
}
}
這是一個非常簡單的MVP開發(fā)模式下的一個聯(lián)網(wǎng)請求代碼蜒秤,用于請求用戶的信息數(shù)據(jù),我們不需要強(qiáng)制性理解每一行代碼贝咙,我們需要關(guān)注的問題是样悟,我們?nèi)绾伪WC這些代碼的正確性,即使在一些邊界條件的限制下庭猩?
單元測試能夠讓項目中自己的代碼更加健壯窟她,但是無論是RxJava+Retrofit,亦或是okHttp3,異步操作的測試都是一個較高的門檻眯娱,加上目前國內(nèi)開發(fā)環(huán)境的惡劣環(huán)境(周期短礁苗,需求變動反復(fù)),國內(nèi)的相關(guān)技術(shù)文檔相當(dāng)匱乏徙缴。
Espresso的異步測試
筆者曾經(jīng)花費一些時間研究Espresso的異步測試试伙,并根據(jù)google官方的demo嘗試總結(jié)在設(shè)備上進(jìn)行測試異步操作的方案:
Android 自動化測試 Espresso篇:異步代碼測試
這是可行的嘁信,但是有一些問題需要注意:
1.Espresso本質(zhì)是依賴設(shè)備所進(jìn)行的集成測試,這種測試是業(yè)務(wù)級的疏叨,測試的內(nèi)容是一個整體的功能潘靖,相對于代碼(方法)級別的單元測試,覆蓋范圍太大蚤蔓。
2.Espresso的測試依賴設(shè)備卦溢,所以進(jìn)行一次完整的測試所消耗的時間更長。
3.Espresso更擅長UI的測試秀又,而對于業(yè)務(wù)代碼來說单寂,Espresso并不擅長。
現(xiàn)在我們需要一種新的方式去測試業(yè)務(wù)代碼吐辙,比如Junit+Mockito.
異步測試的難題
在嘗試RxJava的單元測試時宣决,不可避免的,你會遇到兩個問題:
1.如何測試異步代碼昏苏?
2.如何測試AndroidSchedulers.mainThread()尊沸?
1.異步操作如何轉(zhuǎn)為同步進(jìn)行單元測試?
前者的問題我們已經(jīng)在之前的Android 自動化測試 Espresso篇:異步代碼測試這篇文章中提到過了贤惯,那就是:
在我們還沒有獲取到數(shù)據(jù)進(jìn)行驗證時洼专,測試代碼已經(jīng)跑完了。
以下面一段代碼為例:
//開啟一個子線程孵构,每隔一秒發(fā)送一個數(shù)據(jù)屁商,共發(fā)射10次,結(jié)果輸出 0~9
@Test
fun test() {
Observable.intervalRange(0, 10, 0, 1, TimeUnit.SECONDS)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(i -> System.out.println(i))
}
理所當(dāng)然的認(rèn)為,我們能看到輸出臺上輸出0~9浦译,事實上棒假,數(shù)據(jù)還沒有輸出一個,測試代碼就已經(jīng)跑完了精盅,所以結(jié)果是帽哑,輸出臺空空如也。
單純的使用Thread.sleep(10000)使當(dāng)前的線程停留10秒不一定是最簡單叹俏,但一定是最差的選擇妻枕,因為這意味著我們需要花費更長時間去等待測試結(jié)果,更重要的問題是粘驰,并非每次我們都能直到異步操作的所需要的準(zhǔn)確時間屡谐。
RxJavaPlugins
很快我找到了這篇文章:
這篇文章是為數(shù)不多對我測試RxJava有用的文章之一,作者使用RxJavaPlugins蝌数,通過將RxJava所使用的線程換成Schedulers.trampoline()的方式愕掏,強(qiáng)制立即進(jìn)行當(dāng)前的任務(wù),從而得到單元測試的結(jié)果顶伞。
代碼如下:
/**
* 單元測試的時候饵撑,利用RxJavaPlugins將io線程轉(zhuǎn)換為trampoline
* trampoline應(yīng)該是立即執(zhí)行的意思(待商榷)剑梳,替代了Rx1的immediate。
*/
public static void asyncToSync() {
RxJavaPlugins.reset();
RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() {
@Override
public Scheduler apply(Scheduler scheduler) throws Exception {
return Schedulers.trampoline();
}
});
然后再在測試類里加這么一句:
@Before
public void setUp(){
//將rx異步轉(zhuǎn)同步
RxjavaFactory.asyncToSync();
}
令我振奮的是滑潘,我成功得到了預(yù)期的結(jié)果垢乙。
TestScheduler
我們通過上面的代碼,解決了一大部分的異步測試需求语卤,但是還有一個問題追逮,那就是測試的時間,我們不希望這個測試花費這么長時間粹舵。
RxJava2的開發(fā)人員已經(jīng)為我們考慮到了這個問題钮孵,對此他們提供了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()以絕對的方式調(diào)整時間
fun advanceTimeTo(delayTime: Long, timeUnit: TimeUnit) {
testScheduler.advanceTimeTo(delayTime, timeUnit)
}
//advanceTimeBy()方法將調(diào)度器的時間調(diào)整為相對于當(dāng)前位置的時間
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中的一個屬性电爹。
現(xiàn)在 我們再次進(jìn)行測試:
class RxSchedulerRule_Test {
@Rule
@JvmField
val rxRule = RxSchedulerRule()
@Test
fun testAdvanceTo_complete() {
val observer = startRangeLoopThread()
//將時間軸跳到第10秒蔫仙,模擬執(zhí)行發(fā)生的事件
rxRule.advanceTimeTo(10, TimeUnit.SECONDS)
assertComplete(observer)
}
private fun startRangeLoopThread(): TestObserver<Long> {
val observer = TestObserver<Long>()
val observable = getThread()
observable.subscribe(observer)
return observer
}
//開啟一個子線程,每隔一秒發(fā)送一個數(shù)據(jù)丐箩,共發(fā)射10次,結(jié)果 0~9
private fun getThread(): Observable<Long> {
return Observable.intervalRange(0, 10, 0, 1, TimeUnit.SECONDS)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.newThread())
}
//斷言
private fun assertComplete(observer: TestObserver<Long>) {
observer.assertValueCount(10)
.assertComplete()
.assertNoErrors()
.assertValueAt(0, 0)
.assertValueAt(9, 9)
}
}
這樣我們僅僅需要數(shù)十或者數(shù)百毫秒摇邦,就能準(zhǔn)確測試這個異步操作,并且驗證我們預(yù)期的結(jié)果屎勘。
如何測試AndroidSchedulers.mainThread()
單純的直接測試會導(dǎo)致報錯:找不到AndroidSchedulers這個類施籍。
我們將RxSchedulerRule 中的兩行注解代碼打開,這意味著當(dāng)我們要使用AndroidSchedulers這個類時概漱,RxAndroidPlugins已經(jīng)用TestScheduler替換了AndroidSchedulers.mainThread()丑慎。
這樣就解決了這個問題。
但是我很快遇到了一個奇怪的現(xiàn)象瓤摧,那就是
當(dāng)我一次運行整個測試類時竿裂,只有第一個測試通過,其余的測試通常會失敗照弥。
這個現(xiàn)象困擾我很久腻异,最終我找到的解決方式是,通過依賴注入解決問題:
參考 【譯】使用Kotlin和RxJava測試MVP架構(gòu)的完整示例 - 第2部分 @ditclear
首先要做到這一點这揣,我們需要創(chuàng)建一個SchedulerProvider接口悔常,并提供兩個實現(xiàn)影斑。
- AppSchedulerProvider - 這將為我們提供真正的調(diào)度器。 我們將把這個類注入所有的presenter这嚣,這將為我們的Rx訂閱提供調(diào)度器鸥昏。
- TestSchedulerProvide - 這個類將為我們提供一個TestScheduler而不是真正的scheduler。 當(dāng)我們在測試中實例化我們的presenter時姐帚,我們將使用它作為它的構(gòu)造函數(shù)參數(shù)吏垮。
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
}
通過依賴注入,在生產(chǎn)代碼中使用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<UserInfo>()
retrofit.path = MockAssest.USER_DATA
doReturn(RxTestTransformer<UserInfo>()).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<Any>()).whenever(presenter).bindViewMaybe<Any>(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<UserInfo> = 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("用戶信息為空");
}
}
小結(jié)
文筆所限九秀,只是把思路整理一遍寫了出來遗嗽,很多類都沒有詳細(xì)去講解,比如TestScheduler鼓蜒,比如TestObserver,這都是RxJava提供好的工具痹换,我們一定要好好利用。
雖然思路有些天馬行空都弹,但是筆者最近數(shù)日整理出來的一套Kotlin的RxJava2+Retrofit的單元測試腳手架已經(jīng)搭建完畢,并且作為筆者個人MVP架構(gòu)下的測試框架娇豫,配以詳細(xì)的單元測試代碼,上傳到了Github上畅厢,有需要的朋友可以參考:
參考文章
測試RxJava2
http://www.infoq.com/cn/articles/Testing-RxJava2
【譯】使用Kotlin和RxJava測試MVP架構(gòu)的完整示例 - 第2部分:
http://www.reibang.com/p/0a845ae2ca64