摘要:,他會寫大量的單元測試,甚至達到。當時崇拜之極,卻仍然覺得寫單元測試是很麻煩的一件事情。很多人甚至說離開了單元測試,他們便沒有辦法寫代碼。這些都讓我對單元測試的好感度逐漸的上升。
作為一只本科非計算機專業的程序猿,手動寫單元測試是我從來沒接觸過的東西,甚至在幾個月前,我都不知道單元測試是什么東西。倒不是說沒聽過這個詞,也不是不知道它的大概是什么東西——“用來測試一個方法,或者是一小塊代碼的測試代碼”。然而真正是怎么做的?我并沒有一個概念,或者說并沒有一個感覺。
記得第一份工作在創新工場的時候,聽當時的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》是的,你沒看錯,他說單元測試可以節約時間,提高開發速度!!!身為一個無可救藥的懶癌患者,看了這句話簡直就像看到了一道神光似的!既然都可以節省時間,那肯定是要看看的?。?/p>
有趣的是,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的包,這些可以在你的android_sdk_home/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相關的代碼做測試,又可以很快的運行這些測試呢?
解決的辦法就是使用一個開源的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,需要做幾步配置工作。
首先需要將它和JUnit4加到你項目的dependencies里面,
testCompile "junit:junit:4.12" testCompile ’org.robolectric:robolectric:3.0-rc3’
其中的Robolectric的最新版本號可能會變,具體可以上jcenter查看一下當前的最新版本號。
將Build Variant里面的Test Artifact選擇為Unit Test,如果你找不到Build Variant,可以在菜單欄選擇View -> Tool Windows -> Build Variant. 正常情況下它會出現在左下角。
如果是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:preCompileReleaseUnitTestJava :app:preReleaseUnitTestBuild UP-TO-DATE :app:prepareReleaseUnitTestDependencies :app:processReleaseUnitTestJavaRes UP-TO-DATE :app:compileReleaseUnitTestJava UP-TO-DATE :app:compileReleaseUnitTestSources UP-TO-DATE :app:assembleReleaseUnitTest UP-TO-DATE :app:testRelease UP-TO-DATE :app:test UP-TO-DATE BUILD SUCCESSFUL Total time: 1.45 secs
在我的機器上(MacBook Air 2013款,8G內存,算比較低的配置),運行這個test只需要不到2秒鐘,這才是TDD該有的速度。
注:第一次運行可能需要下載一些library,或者是gradle本身,可能需要花一點時間,這個跟unit test本身沒關。
整個項目已經放到github上面:robolectric-demo
總體來說,Robolectric是個非常強大好用的unit testing framework。雖然使用的過程中肯定也會遇到問題,我個人就遇到不少問題,尤其是跟第三方的library比如Retrofit、ActiveAndroid結合使用的時候,會有不少問題,但瑕不掩瑜,我們依然可以用它完成很大部分的unit testing工作。
有任何意見或建議,或者發現文中任何問題,歡迎留言評論!
作者 小創 更多文章 | Github | 公眾號
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/64380.html
摘要:言歸正傳,上一篇文章單元測試如何開始介紹了幾款單元測試框架基本用法依賴隔離概念,本篇主要解答單元測試中幾個重要問題。在單元測試交流微信群,很多新進來的小伙伴,都會幾個大同小異的問題。 showImg(/img/bVEpaD?w=1080&h=715); 原文鏈接:http://www.jianshu.com/p/f5d197a4d83a 前言 已經一個月沒寫文章了,由于9月份在plan...
閱讀 916·2019-08-30 15:54
閱讀 1479·2019-08-30 15:54
閱讀 2407·2019-08-29 16:25
閱讀 1301·2019-08-29 15:24
閱讀 755·2019-08-29 12:11
閱讀 2513·2019-08-26 10:43
閱讀 1236·2019-08-26 10:40
閱讀 474·2019-08-23 16:24