Android單元測試 - 幾個重要問題
原文鏈接:http://www.jianshu.com/p/f5d197a4d83a
前言
已經一個月沒寫文章了,由于9月份在plan國慶旅行計劃,國慶前前后后去了14天旅行,所以沒時間寫,哈哈。
言歸正傳,上一篇文章《Android單元測試 - 如何開始?》介紹了幾款單元測試框架、Junit & Mockito基本用法、依賴隔離 & Mock概念,本篇主要解答單元測試中幾個重要問題。
在單元測試交流微信群,很多新進來的小伙伴,都會幾個大同小異的問題。我們幾個老鳥們答完一次又一次(厚顏無恥地把自己算上^_^),筆者是有點不耐煩了,后來就等其他同學回答他們.....其實大家提的問題,歸根到底就是“依賴問題”,jvm依賴還是android依賴?用到native方法報錯怎么辦?靜態(tài)方法怎么解決?
于是呢,筆者決定專門寫一篇文章,來講解這幾個問題。
- 如何解決Android依賴?
- 隔離Native方法
- 解決內部new對象
- 靜態(tài)方法
- RxJava異步轉同步
1.如何解決Android依賴?
小白:“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目錄下,創(chuàng)建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下創(chuàng)建,不然會提示Duplicate class found in the file...。單元測試運行妥妥的:
原理很簡單,jvm運行時會找android.text.TextUtils類,然后找isEmpty方法執(zhí)行。學過java反射的同學都知道,只要知道包名類名,就可以拿到Class,知道該類某方法名,就可以獲取Method并執(zhí)行。jvm也是類似的機制,只要我們給一個包名類名與android sdk相同的類,寫上方法名&參數&返回值相同的方法,jvm就能編譯并執(zhí)行。
(提示:android的View之類也能這么搞噢)
2.隔離Native方法
小白:“我用到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,下面會講到更常用的場景。
3.解決內部new對象
小白:“我在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就不存在多個構造函數了,都是構造傳參。
4.靜態(tài)方法
小白:“大神,我在Presenter用到靜態(tài)方法....”筆者:“行了,知道你要說什么。”
Presenter:
- public class Presenter {
- public String getSignParams(int uid, String name, String token) {
- return SignatureUtils.sign(uid, name, token);
- }
- }
解決方法跟上面【解決內部new對象】大同小異,核心思想還是依賴隔離。
1).把sign(...)改成非靜態(tài)方法;
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);
- }
- }
5.RxJava異步轉同步
小白:“大神...”
筆者:“為師掐指一算,料汝會遇此劫難。”
小白:(傳說中從入門到出家?)
- 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:
你會發(fā)現沒有輸出"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開發(fā)團隊已經為大家想好了,提供了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",感謝上帝啊!(應該打賞下筆者吧^_^)
讀者有沒發(fā)現RxTools.asyncToSync()多加了一句RxJavaHooks.setOnComputationScheduler(schedulerFunc),意思將computation線程切換為immediate線程。筆者發(fā)現,僅僅添加RxJavaHooks.setOnIOScheduler(schedulerFunc),對于有delay的Obserable還是未通過,于是順手把computation線程也切換了,于是就可以了。
還有RxJavaHooks.reset()和RxAndroidPlugins.getInstance().reset(),筆者發(fā)現,當運行大量單元測試時,有些會失敗,但單獨運行失敗的單元測試,又通過了。百思不得其解后,添加了那兩句.....可以了!
(關于RxJavaHooks和RxAndroidPlugins的使用,在很久前的文章 《(MVP+RxJava+Retrofit)解耦+Mockito單元測試 經驗分享》已經提及過)
小結
筆者:“小白同學,現在你踩過的坑,填好未?”
小白:“方丈,啊不,大神,上面幾個問題是解決了,不過還有其他問題。”
筆者:“不挖坑,怎么填坑呢?以后再給你講講其他單元測試的玄機。”
小白:“......”
本文詳述了幾個單元測試重要問題的解決方法,讀者不難發(fā)現,筆者一直強調 依賴隔離、依賴隔離、依賴隔離,這個概念在單元測試中相當重要。還搞不懂這個概念的同學,看多幾次《Android單元測試 - 如何開始?》(又厚顏無恥地廣告),同時在實踐中不斷回顧這個理念。
只要解決好這幾個問題,Presenter單元測試就不難了。還有本文未提及的sqlite、SharedPreferences單元測試、在后面的文章會給讀者介紹下。
感謝讀者對筆者一直以來的支持,麻煩點贊&隨手轉發(fā),好人一世平安。
關于作者
我是鍵盤男。在廣州生活,在創(chuàng)業(yè)公司上班,猥瑣文藝碼農。喜歡科學、歷史,玩玩投資,偶爾獨自旅行。希望成為獨當一面的工程師。