Android單元測試(7):Robolectric,在JVM上呼叫安卓的類

小創發表於2016-06-08

今天講講Android上做單元測試的最後一個難點,那就是在JVM上無法呼叫安卓相關的類,不然的話,會報類似於下的錯誤: java.lang.RuntimeException: Method isEmpty in android.text.TextUtils not mocked.

關於這個話題,其實我以前是寫過的,也許今天我回過頭來寫這個話題,會採用不一樣的形式,不一樣的心態來寫,然而,作為我寫過的第一篇關於單元測試的文章,而且看看時間,是去年的6月15號,再過幾天,剛好一週年。想想這篇文章是在我剛開始探索,嘗試在安卓上面寫單元測試的時候,寫的一篇文章,如今因為安卓單元測試的原因,我認識了很多同行,甚至不時有人叫我“大牛大神”之類的,雖然知道大家是客氣,我也受之有愧,但怎麼滴心裡也有點虛榮的開心,哈哈哈。。因此現在回過頭去看看當時自己寫的東西,不禁覺得有點那啥。。。因此,我決定把之前的文章稍作補充和修改,作為這個系列的第七篇。———————-以下文字寫於去年今天———————–

作為一隻本科非計算機專業的程式猿,手動寫單元測試是我從來沒接觸過的東西,甚至在幾個月前,我都不知道單元測試是什麼東西。倒不是說沒聽過這個詞,也不是不知道它的大概是什麼東西——“用來測試一個方法,或者是一小塊程式碼的測試程式碼”。然而真正是怎麼做的?我並沒有一個概念,或者說並沒有一個感覺。

記得第一份工作在創新工場的時候,聽當時的boss說,公司有個神級的程式設計師,他會寫大量的單元測試,甚至50%以上的程式碼都是單元測試。當時崇拜之極,卻仍然覺得寫單元測試是很麻煩的一件事情。

扯遠了,話說回來,當你接觸多了國外的技術部落格,視訊之後,你會發現,單元測試甚至TDD,在國外是非常流行的事情。很多人甚至說離開了單元測試,他們便沒有辦法寫程式碼。這些都讓我對單元測試的好感度逐漸的上升。然而,真正讓我下定決心,一定要研究一下這個東西的,是前段時間看大名鼎鼎的《重構:改善現有程式碼的藝術》裡面的一段話:

I’ve found that writing good tests greatly speeds my programming, even if I’m not refactoring. This was a surprise for me, and it is counterintuitive for many programmers…
–Martin Fowler 《Refactoring: Improving the Design of Existing Code》

是的,你沒看錯,他說單元測試可以節約時間,提高開發速度!!!身為一個無可救藥的懶癌患者,看了這句話簡直就像看到了一道神光似的!既然都可以節省時間,那肯定是要看看的啊!

有趣的是,Martin Fowler在《重構》裡面說他最初是因為 Dave Thomas說的一句話,讓他走上了單元測試的不歸路。而我這幾天剛好又在看Dave Thomas寫的《Programming Ruby 1.9 & 2.0》,也算是個巧合啊!

Martin Fowler在《重構》裡面還解釋了為什麼單元測試可以節省時間,大意是我們寫程式的時候,其實大部分時間不是花在寫程式碼上面,而是花在debug上面,是花在找出問題到底出在哪上面,而單元測試可以最快的發現你的新程式碼哪裡不work,這樣你就可以很快的定位到問題所在,然後給以及時的解決,這也可以在很大程度上防止regression(相信QE和QA們一定很喜歡哈哈。。。),這也是個大部分程式設計師和測試都很痛恨的問題。

之後不久,就開始花了點時間瞭解了一下Android裡面怎麼做unit testing,結果卻發現那是個非常難辦的事情。。。

為什麼android unit testing不好做

我們知道安卓的app需要執行在delvik上面,我們開發Android app是在JVM上面,在開發之前我們需要下載各個API-level的SDK的,下載的每個SDK都有一個android.jar的包,這些可以在你的androidsdkhome/platforms/下面看到。當我們開發一個專案的時候,我們需要指定一個API-level,其實就是將對應的android.jar 加到這個專案的build path裡面去。這樣我們的專案就可以編譯打包了。然而現在的問題是,我們的程式碼必須執行在emulator或者是device上面,說白了,就是我們的IDE和SDK只提供了開發和編譯一個專案的環境,並沒有提供執行這個專案的環境,原因是因為android.jar裡面的class實現是不完整的,它們只是一些stub,如果你開啟android.jar下面的程式碼去看看,你會發現所有的方法都只有一行實現:

throw RuntimeException("stub!!");

而執行unit test,說白了還是個執行的過程,所以如果你的unit test程式碼裡面有android相關的程式碼的話,那執行的時候將會丟擲RuntimeException(“stub!!”)。為了解決這個問題,現在業界提出了很多不同的程式架構,比如MVP、MVVM等等,這些架構的優勢之一,就是將其中一層抽出來,變成pure Java實現,這樣做unit testing就不會遇到上面這個問題了,因為其中沒有android相關的程式碼。
好奇的童鞋可能會問了,既然android.jar的實現是不完整的,那為什麼我們可以編譯這個專案呢?那是因為編譯程式碼的過程並沒有真正的執行這些程式碼,它只會檢查你的介面有沒有定義,以及其他的一些語法是不是正確。舉個簡單的例子:

上面的程式碼你同樣可以編譯通過,但你執行的時候,就會丟擲異常RuntimeException("stub!!")。當我們的專案執行在emulator或者是device上面的時候,android.jar被替換成了emulator或者是device上面的系統的實現,那上面的實現是真正實現了那些方法的,所以執行起來沒有問題。

話說回來,MVP、MVVM這些架構模式雖然解決了部分問題,可以測試專案中不含android相關的類的程式碼,然而一個專案中還是有很大部分是android相關的程式碼的,所以上面那種解決方案,其實是放棄了其中一大塊程式碼的unit test。

當然,話說回來,android還是提供了他自己的testing framework,叫instrumentation,但是這套框架還是繞不開剛剛提到的問題,他們必須跑在emulator或者是device上面。這是個很慢的過程,因為要打包、dexing、上傳到機器、執行起來介面。。。這個相信大家都有體會,尤其是專案大了以後,執行一次甚至需要一兩分鐘,專案小的話至少也要十幾秒或幾十秒。以這個速度是沒有辦法做unit test的。

那麼怎麼樣即可以給android相關的程式碼做測試,又可以很快的執行這些測試呢?

Robolectric to the rescue

解決的辦法就是使用一個開源的framework,叫robolectric,他們的做法是通過實現一套JVM能執行的Android程式碼,然後在unit test執行的時候去擷取android相關的程式碼呼叫,然後轉到他們的他們實現的程式碼去執行這個呼叫的過程。舉個例子說明一下,比如android裡面有個類叫TextView,他們實現了一個類叫ShadowTextView。這個類基本上實現了TextView的所有公共介面,假設你在unit test裡面寫到

String text = textView.getText().toString();。在這個unit test執行的時候,Robolectric會自動判斷你呼叫了Android相關的程式碼textView.getText(),然後這個呼叫過程在底層擷取了,轉到ShadowTextViewgetText實現。而ShadowTextView是真正實現了getText這個方法的,所以這個過程便可以正常執行。

除了實現Android裡面的類的現有介面,Robolectric還做了另外一件事情,極大地方便了unit testing的工作。那就是他們給每個Shadow類額外增加了很多介面,可以讀取對應的Android類的一些狀態。比如我們知道ImageView有一個方法叫

setImageResource(resourceId),然而並沒有一個對應的getter方法叫getImageResourceId(),這樣你是沒有辦法測試這個ImageView是不是顯示了你想要的image。而在Robolectric實現的對應的ShadowImageView裡面,則提供了getImageResourceId()這個介面。你可以用來測試它是不是正確的顯示了你想要的Image。

Talk is cheap. Show me the code!

下面簡單的介紹一下使用Robolectric來做unit testing。注意:下面的配置方法指的是AndroidStudio上面的,Eclipse使用者自行google一下配製方法。

要使用Robolectric,需要做幾步配置工作。

  1. 首先需要將它和JUnit4加到你專案的dependencies裡面,

其中的Robolectric的最新版本號可能會變,具體可以上jcenter檢視一下當前的最新版本號。

2. 如果你用的是AndroidStudio2.0一下的版本,需要將Build Variant裡面的Test Artifact選擇為Unit Test,如果你找不到Build Variant,可以在選單欄選擇View -> Tool Windows -> Build Variant. 正常情況下它會出現在左下角。AndroidStudio2.0以上的版本已經不需要了。

3. 如果是Mac的話,還需要配置一個東西,選單欄選擇 Run -> Edit Configuration -> Defaults -> JUnit,在Configuration tab將working directory改成$MODULE_DIR$。這個配置是Robolectric官方文件提到的,但我用最新的AndroidStudio1.3實驗的時候,忘了配置這個,貌似也可以正確執行,anyway,配置一下也無所謂。具體見Robolectric的官方文件,最下面那部分。

到這裡,就可以開始code了。

測試程式碼是放在app/src/test下面的,test class的位置最好跟target class的位置對應,比如MainActivity放在
app/src/main/java/com/domain/appname/MainActivity.java

那麼對應的test class MainActivityTest最好放在
app/src/test/java/com/domain/appname/MainActivityTest.java

這裡舉個簡單又稍微有點用的例子,假設app裡面有兩個Activity:MainActivitySecondActivityMainActivity裡面有一個TextView,點選一下這個TextView將跳轉到SecondActivityMainActivity裡面的程式碼大概如下:

對應的測試類,MainActivityTest的程式碼:

上面的程式碼測試的就是當使用者點選textView的時候,程式會正確的跳轉到SecondActivity。其中@RunWith(RobolectricGradleTestRunner.class)表示用Robolectric的TestRunner來跑這些test,這就是為什麼Robolectric可以檢測到你呼叫了Android相關的類,然後擷取這些呼叫,轉到他們的Shadow類的原因。此外,@Config用來配置一些東西。

程式碼中的
MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class); 用來建立MainActivity的instance,或者說,用來啟動這個Activity,當Robolectric.setupActivity返回的時候,這個Activity已經完成了onCreate、onStart、onResume這幾個生命週期的回撥了。

mainActivity.findViewById(R.id.textView1).performClick();用來觸發點選事件。ShadowActivity shadowActivity = Shadows.shadowOf(mainActivity);用來獲取mainActivity對應的ShadowActivity的instance。
shadowActivity.getNextStartedActivity();用來獲取mainActivity呼叫的startActivity的intent。這也是正常的Activity類裡面不具有的一個介面。

最後,呼叫Assert.assertEquals來assert啟動的intent是我們期望的intent。

執行這個unit test,啟動命令列,cd到專案的根目錄,執行./gradlew test ,幾秒鐘後,你將看到測試執行的結果

在我的機器上(MacBook Air 2013款,8G記憶體,算比較低的配置),執行這個test只需要不到12秒鐘,如果直接在AndroidStudio裡面執行的話,這個速度會更快,一般可以再10秒之內完成,或許沒有達到普通JUnit的秒級速度,然而相對於用Instrumentation來說已經是極大的提升了。

注:第一次執行可能需要下載一些library,或者是gradle本身,可能需要花一點時間,這個跟unit test本身沒關。
整個專案已經放到github上面:robolectric-demo

小結

總體來說,Robolectric是個非常強大好用的unit testing framework。雖然使用的過程中肯定也會遇到問題,我個人就遇到不少問題,尤其是跟第三方的library比如Retrofit、ActiveAndroid結合使用的時候,會有不少問題,但瑕不掩瑜,我們依然可以用它完成很大部分的unit testing工作。

————————–原文結束————————–

今天回過頭來看,我想強調的是,Robolectric到底應該充當什麼樣的一個角色。在沒有Robolectric的pure JUnit世界,我們是很難對一整個流程進行測試的,因為上層的介面是安卓的類,底層的資料庫和Preference等等是安卓的類。因此,我們沒有辦法對一整個流程做一個完整的測試。然而有了robolectric以後,我們就可以這麼做了:啟動activity,向網路或資料庫請求資料,更新介面。。。因此,有了這個東西以後,我們的第一反應可能就是去測試這整個app流程。所以經常有小夥伴問我,Robolectric到底是做單元測試的框架,還是做整合測試,甚至UI測試的框架?

這就是我想強調的,需要避免的陷阱。對於上面的問題,我的回答是:Robolectric就是一個能夠讓我們在JVM上跑 測試 時夠呼叫安卓的類的框架,至於我們是拿它來做單元測試還是整合測試,完全取決於我們自己。而回到我們強調的 單元測試,測一個小的獨立的程式碼單元,Robolectric的角色,應該是一個讓我們在做 單元測試 的過程中,能夠呼叫安卓的類,測試安卓的類,把安卓的類當做普通的純java類的一個framework,僅此而已。

這點,謹記。

有任何意見或建議,或者發現文中任何問題,歡迎留言評論!

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

Android單元測試(7):Robolectric,在JVM上呼叫安卓的類

相關文章