原文連接:http://www.jianshu.com/p/f5d197a4d83ajava
已經一個月沒寫文章了,因爲9月份在plan國慶旅行計劃,國慶前先後後去了14天旅行,因此沒時間寫,哈哈。android
言歸正傳,上一篇文章《Android單元測試 - 如何開始?》介紹了幾款單元測試框架、Junit & Mockito基本用法、依賴隔離 & Mock概念,本篇主要解答單元測試中幾個重要問題。git
在單元測試交流微信羣,不少新進來的小夥伴,都會幾個大同小異的問題。咱們幾個老鳥們答完一次又一次(厚顏無恥地把本身算上^_^),筆者是有點不耐煩了,後來就等其餘同窗回答他們.....其實你們提的問題,歸根到底就是「依賴問題」,jvm依賴仍是android依賴?用到native方法報錯怎麼辦?靜態方法怎麼解決?sql
因而呢,筆者決定專門寫一篇文章,來說解這幾個問題。微信
如何解決Android依賴?框架
隔離Native方法異步
解決內部new對象jvm
靜態方法async
RxJava異步轉同步ide
小白:「Presenter中用到TextUtils,運行junit時報'java.lang.RuntimeException: Method isEmpty in android.text.TextUtils not mocked'錯誤... 是否是要用robolectric?」
別急,還未到robolectric出場的時候呢!
因爲junit運行在jvm上,而jdk沒有android源碼,因此TextUtils這些在android sdk中的類,運行junit時就引用不上了。既然jdk沒有,咱們就本身加唄!
在
test/java
目錄下,建立android.text.TextUtils
類
package android.text; public class TextUtils { public static boolean isEmpty(CharSequence str) { if (str == null || str.equals("")) { return true; } return false; } }
關鍵是要個TextUtils
同包名、同類名、同方法名。注意不是在main/java
下建立,否則會提示Duplicate class found in the file...
。單元測試運行妥妥的:
原理很簡單,jvm運行時會找android.text.TextUtils
類,而後找isEmpty
方法執行。學過java反射的同窗都知道,只要知道包名類名,就能夠拿到Class
,知道該類某方法名,就能夠獲取Method
並執行。jvm也是相似的機制,只要咱們給一個包名類名與android sdk相同的類,寫上方法名&參數&返回值相同的方法,jvm就能編譯並執行。
(提示:android的View之類也能這麼搞噢)
小白:「我用到native方法,junit運行失敗,robolectric也不支持加載so文件,怎麼辦?」
Model類:
package com.test.unit; public class Model { public native boolean nativeMethod(); }
單元測試:
public class ModelTest { Model model; @Before public void setUp() throws Exception { model = new Model(); } @Test public void testNativeMethod() throws Exception { Assert.assertTrue(model.nativeMethod()); } }
run ModelTest
... 報錯java.lang.UnsatisfiedLinkError: com.test.unit.Model.nativeMethod()
上篇文章《Android單元測試 - 如何開始?》講述的「依賴隔離」,這裏要用到了!
改進單元測試:
public class ModelTest { Model model; @Before public void setUp() throws Exception { model = mock(Model.class); } @Test public void testNativeMethod() throws Exception { when(model.nativeMethod()).thenReturn(true); Assert.assertTrue(model.nativeMethod()); } }
再run
一下,pass了:
這裏稍微講講java查找native方法的過程:
1.Model.java
全名是com.test.unit.Model.java
;
2.調用native方法nativeMethod()
後, jvm會去找C++層com_test_unit_Model.cpp
,再找com_test_unit_Model_nativeMethod()
方法,並調用。
在APP運行過程,咱們會把cpp編譯成so文件,而後讓APP加載到dalvik虛擬機。但在單元測試中,沒有加載對應的so文件,也沒有編譯cpp呀!大牛們可能會嘗試單元測試時加載so文件,但徹底沒有必要,也不符合單元測試的原則。
因此,咱們能夠直接用Mockito框架mock native方法就行啦。實際上,不只僅是native方法須要mock,不少依賴的方法、類都要mock,下面會講到更經常使用的場景。
小白:「我在Presenter裏new Model,Model依賴比較多,會作sql操做,等等.....Presenter依賴Model返回結果,致使Presenter無法單元測試啦!求大神指點!」
小白C的例子:
Model:
public class Model { public boolean getBoolean() { boolean bo = ....... // 一堆依賴,代碼很複雜 return bo; } }
Presenter:
public class Presenter { Model model; public Presenter() { model = new Model(); } public boolean getBoolean() { return model.getBoolean()); } }
錯誤的單元測試:
public class PresenterTest { Presenter presenter; @Before public void setUp() throws Exception { presenter = new Presenter(); } @Test public void testGetBoolean() throws Exception { Assert.assertTrue(presenter.getBoolean()); } }
仍是那句話:依賴隔離。咱們隔離Model
依賴,即mock Model對象
,而不是new Model()
。
找找以上PresenterTest
的問題吧:PresenterTest
徹底不知道Model
的存在,意思是沒法mock Model
。那麼,咱們就想辦法把mock Model
傳給Presenter
——在Presenter
構造函數傳參!
改進Presenter
:
public class Presenter { Model model; public Presenter(Model model) { this.model = model; } public boolean getBoolean() { return model.getBoolean(); } }
正確的單元測試:
public class PresenterTest { Model model; Presenter presenter; @Before public void setUp() throws Exception { model = mock(Model.class);// mock Model對象 presenter = new Presenter(model); } @Test public void testGetBoolean() throws Exception { when(model.getBoolean()).thenReturn(true); Assert.assertTrue(presenter.getBoolean()); } }
事情就這麼解決了。若是你以爲在Activity直接用默認Presenter
構造函數,在構造函數new Model()
比較方便,那就保留默認構造函數唄。固然使用dagger2就不存在多個構造函數了,都是構造傳參。
小白:「大神,我在Presenter用到靜態方法....」
筆者:「行了,知道你要說什麼。」
Presenter:
public class Presenter { public String getSignParams(int uid, String name, String token) { return SignatureUtils.sign(uid, name, token); } }
解決方法跟上面【解決內部new對象】大同小異,核心思想仍是依賴隔離。
1.把sign(...)
改爲非靜態方法;
2.把SignatureUtils
做爲成員變量;
3.構造方法傳入SignatureUtils
;
4.單元測試時,把mock SignatureUtils
傳給Presenter
。
改進後Presenter
:
public class Presenter { SignatureUtils mSignUtils; public Presenter(SignatureUtils signatureUtils) { this.mSignUtils= signatureUtils; } public String getSignParams(int uid, String name, String token) { return mSignUtils.sign(uid, name, token); } }
小白:「大神...」
筆者:「爲師掐指一算,料汝會遇此劫難。」
小白:(傳說中從入門到出家?)
public class RxPresenter { public void testRxJava(String msg) { Observable.just(msg) .subscribeOn(Schedulers.io()) .delay(1, TimeUnit.SECONDS) // 延時1秒 // .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<String>() { @Override public void call(String msg) { System.out.println(msg); } }); } }
單元測試
public class RxPresenterTest { RxPresenter rxPresenter; @Before public void setUp() throws Exception { rxPresenter = new RxPresenter(); } @Test public void testTestRxJava() throws Exception { rxPresenter.testRxJava("test"); } }
運行RxPresenterTest
:
你會發現沒有輸出"test",爲何呢?
因爲testRxJava
裏面,Obserable.subscribeOn(Schedulers.io())
把線程切換到io線程,而且delay
了1秒,而testTestRxJava()
單元測試早已在當前線程跑完了。筆者試過,即便去掉delay(1, TimeUnit.SECONDS)
,仍是不會輸出‘test’
。
能夠看到筆者把.observeOn(AndroidSchedulers.mainThread())
註釋掉了,咱們把那句代碼加上,再跑一下testTestRxJava()
,會報java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked.
:
這是因爲jdk沒有android.os.Looper這個類及相關依賴。
解決以上兩個問題,咱們只要把Schedulers.io()
&AndroidSchedulers.mainThread()
切換爲Schedulers.immediate()
就能夠了。RxJava開發團隊已經爲你們想好了,提供了RxJavaHooks
和RxAndroidPlugins
兩個hook操做的類。
新建RxTools
:
public class RxTools { public static void asyncToSync() { Func1<Scheduler, Scheduler> schedulerFunc = new Func1<Scheduler, Scheduler>() { @Override public Scheduler call(Scheduler scheduler) { return Schedulers.immediate(); } }; RxAndroidSchedulersHook rxAndroidSchedulersHook = new RxAndroidSchedulersHook() { @Override public Scheduler getMainThreadScheduler() { return Schedulers.immediate(); } }; RxJavaHooks.reset(); RxJavaHooks.setOnIOScheduler(schedulerFunc); RxJavaHooks.setOnComputationScheduler(schedulerFunc); RxAndroidPlugins.getInstance().reset(); RxAndroidPlugins.getInstance().registerSchedulersHook(rxAndroidSchedulersHook); } }
在RxPresenterTest.setUp()
加一句RxTools.asyncToSync();
:
public class RxPresenterTest { RxPresenter rxPresenter; @Before public void setUp() throws Exception { rxPresenter = new RxPresenter(); RxTools.asyncToSync(); } ... }
再跑一次testTestRxJava()
:
總算輸出"test",感謝上帝啊!(應該打賞下筆者吧^_^)
讀者有沒發現RxTools.asyncToSync()
多加了一句RxJavaHooks.setOnComputationScheduler(schedulerFunc)
,意思將computation線程切換爲immediate線程。筆者發現,僅僅添加RxJavaHooks.setOnIOScheduler(schedulerFunc)
,對於有delay
的Obserable
仍是未經過,因而順手把computation線程也切換了,因而就能夠了。
還有RxJavaHooks.reset()
和RxAndroidPlugins.getInstance().reset()
,筆者發現,當運行大量單元測試時,有些會失敗,但單獨運行失敗的單元測試,又經過了。百思不得其解後,添加了那兩句.....能夠了!
(關於RxJavaHooks
和RxAndroidPlugins
的使用,在好久前的文章 《(MVP+RxJava+Retrofit)解耦+Mockito單元測試 經驗分享》已經說起過)
筆者:「小白同窗,如今你踩過的坑,填好未?」
小白:「方丈,啊不,大神,上面幾個問題是解決了,不過還有其餘問題。」
筆者:「不挖坑,怎麼填坑呢?之後再給你講講其餘單元測試的玄機。」
小白:「......」
本文詳述了幾個單元測試重要問題的解決方法,讀者不難發現,筆者一直強調 依賴隔離、依賴隔離、依賴隔離,這個概念在單元測試中至關重要。還搞不懂這個概念的同窗,看多幾回《Android單元測試 - 如何開始?》(又厚顏無恥地廣告),同時在實踐中不斷回顧這個理念。
只要解決好這幾個問題,Presenter單元測試就不難了。還有本文未說起的sqlite、SharedPreferences單元測試、在後面的文章會給讀者介紹下。
感謝讀者對筆者一直以來的支持,麻煩點贊&隨手轉發,好人一世平安。
關於做者
我是鍵盤男。在廣州生活,在創業公司上班,猥瑣文藝碼農。喜歡科學、歷史,玩玩投資,偶爾獨自旅行。但願成爲獨當一面的工程師。