今天講講Android上作單元測試的最後一個難點,那就是在JVM上沒法調用安卓相關的類,否則的話,會報相似於下的錯誤: java.lang.RuntimeException: Method isEmpty in android.text.TextUtils not mocked.
java
關於這個話題,其實我之前是寫過的,也許今天我回過頭來寫這個話題,會採用不同的形式,不同的心態來寫,然而,做爲我寫過的第一篇關於單元測試的文章,並且看看時間,是去年的6月15號,再過幾天,恰好一週年。想一想這篇文章是在我剛開始探索,嘗試在安卓上面寫單元測試的時候,寫的一篇文章,現在由於安卓單元測試的緣由,我認識了不少同行,甚至不時有人叫我「大牛大神」之類的,雖然知道你們是客氣,我也受之有愧,但怎麼滴內心也有點虛榮的開心,哈哈哈。。所以如今回過頭去看看當時本身寫的東西,不由以爲有點那啥。。。所以,我決定把以前的文章稍做補充和修改,做爲這個系列的第七篇。android
----------------------如下文字寫於去年今天-----------------------git
做爲一隻本科非計算機專業的程序猿,手動寫單元測試是我歷來沒接觸過的東西,甚至在幾個月前,我都不知道單元測試是什麼東西。倒不是說沒聽過這個詞,也不是不知道它的大概是什麼東西——「用來測試一個方法,或者是一小塊代碼的測試代碼」。然而真正是怎麼作的?我並無一個概念,或者說並無一個感受。
記得第一份工做在創新工場的時候,聽當時的boss說,公司有個神級的程序員,他會寫大量的單元測試,甚至50%以上的代碼都是單元測試。當時崇拜之極,卻仍然以爲寫單元測試是很麻煩的一件事情。程序員
扯遠了,話說回來,當你接觸多了國外的技術博客,視頻以後,你會發現,單元測試甚至TDD,在國外是很是流行的事情。不少人甚至說離開了單元測試,他們便沒有辦法寫代碼。這些都讓我對單元測試的好感度逐漸的上升。然而,真正讓我下定決心,必定要研究一下這個東西的,是前段時間看大名鼎鼎的《重構:改善現有代碼的藝術》裏面的一段話:github
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,結果卻發現那是個很是難辦的事情。。。網絡
咱們知道安卓的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的實現是不完整的,那爲何咱們能夠編譯這個項目呢?那是由於編譯代碼的過程並無真正的運行這些代碼,它只會檢查你的接口有沒有定義,以及其餘的一些語法是否是正確。舉個簡單的例子:架構
public class Test { public static void main(String[] argv) { testMethod(); } public static void testMethod() { throw RuntimeException("stub!!"); } }
上面的代碼你一樣能夠編譯經過,但你運行的時候,就會拋出異常RuntimeException("stub!!")
。當咱們的項目運行在emulator或者是device上面的時候,android.jar被替換成了emulator或者是device上面的系統的實現,那上面的實現是真正實現了那些方法的,因此運行起來沒有問題。
話說回來,MVP、MVVM這些架構模式雖然解決了部分問題,能夠測試項目中不含android相關的類的代碼,然而一個項目中仍是有很大部分是android相關的代碼的,因此上面那種解決方案,實際上是放棄了其中一大塊代碼的unit test。
固然,話說回來,android仍是提供了他本身的testing framework,叫instrumentation,可是這套框架仍是繞不開剛剛提到的問題,他們必須跑在emulator或者是device上面。這是個很慢的過程,由於要打包、dexing、上傳到機器、運行起來界面。。。這個相信你們都有體會,尤爲是項目大了之後,運行一次甚至須要一兩分鐘,項目小的話至少也要十幾秒或幾十秒。以這個速度是沒有辦法作unit test的。
那麼怎麼樣便可以給android相關的代碼作測試,又能夠很快的運行這些測試呢?app
解決的辦法就是使用一個開源的framework,叫robolectric,他們的作法是經過實現一套JVM能運行的Android代碼,而後在unit test運行的時候去截取android相關的代碼調用,而後轉到他們的他們實現的代碼去執行這個調用的過程。舉個例子說明一下,好比android裏面有個類叫TextView
,他們實現了一個類叫ShadowTextView
。這個類基本上實現了TextView
的全部公共接口,假設你在unit test裏面寫到
String text = textView.getText().toString();
。在這個unit test運行的時候,Robolectric會自動判斷你調用了Android相關的代碼textView.getText()
,而後這個調用過程在底層截取了,轉到ShadowTextView
的getText
實現。而ShadowTextView
是真正實現了getText
這個方法的,因此這個過程即可以正常執行。
除了實現Android裏面的類的現有接口,Robolectric還作了另一件事情,極大地方便了unit testing的工做。那就是他們給每一個Shadow類額外增長了不少接口,能夠讀取對應的Android類的一些狀態。好比咱們知道ImageView
有一個方法叫setImageResource(resourceId)
,然而並無一個對應的getter方法叫getImageResourceId()
,這樣你是沒有辦法測試這個ImageView
是否是顯示了你想要的image。而在Robolectric實現的對應的ShadowImageView
裏面,則提供了getImageResourceId()
這個接口。你能夠用來測試它是否是正確的顯示了你想要的Image。框架
下面簡單的介紹一下使用Robolectric來作unit testing。注意:下面的配置方法指的是AndroidStudio上面的,Eclipse用戶自行google一下配製方法。
要使用Robolectric,須要作幾步配置工做。
testCompile 'junit:junit:4.12' testCompile ’org.robolectric:robolectric:3.0-rc3’
其中的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:MainActivity
和SecondActivity
,MainActivity
裏面有一個TextView
,點擊一下這個TextView
將跳轉到SecondActivity
,MainActivity
裏面的代碼大概以下:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView textView = (TextView)findViewById(R.id.textView1); textView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { startActivity(new Intent(MainActivity.this, SecondActivity.class)); } }); } }
對應的測試類,MainActivityTest
的代碼:
@RunWith(RobolectricGradleTestRunner.class) @Config(constants = BuildConfig.class, sdk = 21) public class MainActivityTest { @Test public void testMainActivity() { MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class); mainActivity.findViewById(R.id.textView1).performClick(); Intent expectedIntent = new Intent(mainActivity, SecondActivity.class); ShadowActivity shadowActivity = Shadows.shadowOf(mainActivity); Intent actualIntent = shadowActivity.getNextStartedActivity(); Assert.assertEquals(expectedIntent, actualIntent); } }
上面的代碼測試的就是當用戶點擊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
,幾秒鐘後,你將看到測試運行的結果
... :app:processDebugJavaRes UP-TO-DATE :app:compileDebugJava UP-TO-DATE :app:preCompileDebugUnitTestJava :app:preDebugUnitTestBuild UP-TO-DATE :app:prepareDebugUnitTestDependencies :app:processDebugUnitTestJavaRes UP-TO-DATE :app:compileDebugUnitTestJava :app:compileDebugUnitTestSources :app:mockableAndroidJar UP-TO-DATE :app:assembleDebugUnitTest :app:testDebug BUILD SUCCESSFUL Total time: 12.884 secs
在個人機器上(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,僅此而已。
這點,謹記。
最後,若是你對安卓單元測試感興趣,歡迎加入咱們的交流羣,由於羣成員超過100人,沒辦法掃碼加入,請關注下方公衆號獲取加入方法。
有任何意見或建議,或者發現文中任何問題,歡迎留言評論!