摘要:,他會(huì)寫大量的單元測(cè)試,甚至達(dá)到。當(dāng)時(shí)崇拜之極,卻仍然覺得寫單元測(cè)試是很麻煩的一件事情。很多人甚至說離開了單元測(cè)試,他們便沒有辦法寫代碼。這些都讓我對(duì)單元測(cè)試的好感度逐漸的上升。
作為一只本科非計(jì)算機(jī)專業(yè)的程序猿,手動(dòng)寫單元測(cè)試是我從來沒接觸過的東西,甚至在幾個(gè)月前,我都不知道單元測(cè)試是什么東西。倒不是說沒聽過這個(gè)詞,也不是不知道它的大概是什么東西——“用來測(cè)試一個(gè)方法,或者是一小塊代碼的測(cè)試代碼”。然而真正是怎么做的?我并沒有一個(gè)概念,或者說并沒有一個(gè)感覺。
記得第一份工作在創(chuàng)新工場(chǎng)的時(shí)候,聽當(dāng)時(shí)的boss @王明禮 說,公司有個(gè)神級(jí)的程序員(。。。名字忘了。。。),他會(huì)寫大量的單元測(cè)試,甚至達(dá)到50%。當(dāng)時(shí)崇拜之極,卻仍然覺得寫單元測(cè)試是很麻煩的一件事情。
扯遠(yuǎn)了,話說回來,當(dāng)你接觸多了國外的技術(shù)博客,視頻之后,你會(huì)發(fā)現(xiàn),單元測(cè)試甚至TDD,在國外是非常流行的事情。很多人甚至說離開了單元測(cè)試,他們便沒有辦法寫代碼。這些都讓我對(duì)單元測(cè)試的好感度逐漸的上升。然而,真正讓我下定決心,一定要研究一下這個(gè)東西的,是前段時(shí)間看大名鼎鼎的《重構(gòu):改善現(xiàn)有代碼的藝術(shù)》里面的一段話:
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》是的,你沒看錯(cuò),他說單元測(cè)試可以節(jié)約時(shí)間,提高開發(fā)速度?。。∩頌橐粋€(gè)無可救藥的懶癌患者,看了這句話簡(jiǎn)直就像看到了一道神光似的!既然都可以節(jié)省時(shí)間,那肯定是要看看的啊!
有趣的是,Martin Fowler在《重構(gòu)》里面說他最初是因?yàn)?Dave Thomas說的一句話,讓他走上了單元測(cè)試的不歸路。而我這幾天剛好又在看Dave Thomas寫的《Programming Ruby 1.9 & 2.0》。。。。。。寫到這里頓時(shí)覺得自己很不要臉。。。。。。
Martin Fowler在《重構(gòu)》里面還解釋了為什么單元測(cè)試可以節(jié)省時(shí)間,大意是我們寫程序的時(shí)候,其實(shí)大部分時(shí)間不是花在寫代碼上面,而是花在debug上面,是花在找出問題到底出在哪上面,而單元測(cè)試可以最快的發(fā)現(xiàn)你的新代碼哪里不work,這樣你就可以很快的定位到問題所在,然后給以及時(shí)的解決,這也可以在很大程度上防止regression(相信QE和QA們一定很喜歡哈哈。。。),這也是個(gè)大部分程序員和測(cè)試都很痛恨的問題。
之后不久,就開始花了點(diǎn)時(shí)間了解了一下Android里面怎么做unit testing,結(jié)果卻發(fā)現(xiàn)那是個(gè)非常難辦的事情。。。
我們知道安卓的app需要運(yùn)行在delvik上面,我們開發(fā)Android app是在JVM上面,在開發(fā)之前我們需要下載各個(gè)API-level的SDK的,下載的每個(gè)SDK都有一個(gè)android.jar的包,這些可以在你的android_sdk_home/platforms/下面看到。當(dāng)我們開發(fā)一個(gè)項(xiàng)目的時(shí)候,我們需要指定一個(gè)API-level,其實(shí)就是將對(duì)應(yīng)的android.jar 加到這個(gè)項(xiàng)目的build path里面去。這樣我們的項(xiàng)目就可以編譯打包了。然而現(xiàn)在的問題是,我們的代碼必須運(yùn)行在emulator或者是device上面,說白了,就是我們的IDE和SDK只提供了開發(fā)和編譯一個(gè)項(xiàng)目的環(huán)境,并沒有提供運(yùn)行這個(gè)項(xiàng)目的環(huán)境,原因是因?yàn)閍ndroid.jar里面的class實(shí)現(xiàn)是不完整的,它們只是一些stub,如果你打開android.jar下面的代碼去看看,你會(huì)發(fā)現(xiàn)所有的方法都只有一行實(shí)現(xiàn):
throw RuntimeException("stub!!”);
而運(yùn)行unit test,說白了還是個(gè)運(yùn)行的過程,所以如果你的unit test代碼里面有android相關(guān)的代碼的話,那運(yùn)行的時(shí)候?qū)?huì)拋出RuntimeException("stub!!”)。為了解決這個(gè)問題,現(xiàn)在業(yè)界提出了很多不同的程序架構(gòu),比如MVP、MVVM等等,這些架構(gòu)的優(yōu)勢(shì)之一,就是將其中一層抽出來,變成pure Java實(shí)現(xiàn),這樣做unit testing就不會(huì)遇到上面這個(gè)問題了,因?yàn)槠渲袥]有android相關(guān)的代碼。
好奇的童鞋可能會(huì)問了,既然android.jar的實(shí)現(xiàn)是不完整的,那為什么我們可以編譯這個(gè)項(xiàng)目呢?那是因?yàn)榫幾g代碼的過程并沒有真正的運(yùn)行這些代碼,它只會(huì)檢查你的接口有沒有定義,以及其他的一些語法是不是正確。舉個(gè)簡(jiǎn)單的例子:
public class Test { public static void main(String[] argv) {? testMethod(); } public static void testMethod() { throw RuntimeException("stub!!”); } }
上面的代碼你同樣可以編譯通過,但你運(yùn)行的時(shí)候,就會(huì)拋出異常RuntimeException("stub!!”)。當(dāng)我們的項(xiàng)目運(yùn)行在emulator或者是device上面的時(shí)候,android.jar被替換成了emulator或者是device上面的系統(tǒng)的實(shí)現(xiàn),那上面的實(shí)現(xiàn)是真正實(shí)現(xiàn)了那些方法的,所以運(yùn)行起來沒有問題。
話說回來,MVP、MVVM這些架構(gòu)模式雖然解決了部分問題,可以測(cè)試項(xiàng)目中不含android相關(guān)的類的代碼,然而一個(gè)項(xiàng)目中還是有很大部分是android相關(guān)的代碼的,所以上面那種解決方案,其實(shí)是放棄了其中一大塊代碼的unit test。
當(dāng)然,話說回來,android還是提供了他自己的testing framework,叫instrumentation,但是這套框架還是繞不開剛剛提到的問題,他們必須跑在emulator或者是device上面。這是個(gè)很慢的過程,因?yàn)橐虬?、dexing、上傳到機(jī)器、運(yùn)行起來界面。。。這個(gè)相信大家都有體會(huì),尤其是項(xiàng)目大了以后,運(yùn)行一次甚至需要一兩分鐘,項(xiàng)目小的話至少也要十幾秒或幾十秒。以這個(gè)速度是沒有辦法做unit test的。
那么怎么樣即可以給android相關(guān)的代碼做測(cè)試,又可以很快的運(yùn)行這些測(cè)試呢?
解決的辦法就是使用一個(gè)開源的framework,叫robolectric,他們的做法是通過實(shí)現(xiàn)一套JVM能運(yùn)行的Android代碼,然后在unit test運(yùn)行的時(shí)候去截取android相關(guān)的代碼調(diào)用,然后轉(zhuǎn)到他們的他們實(shí)現(xiàn)的代碼去執(zhí)行這個(gè)調(diào)用的過程。舉個(gè)例子說明一下,比如android里面有個(gè)類叫TextView,他們實(shí)現(xiàn)了一個(gè)類叫ShadowTextView。這個(gè)類基本上實(shí)現(xiàn)了TextView的所有公共接口,假設(shè)你在unit test里面寫到
String text = textView.getText().toString();。在這個(gè)unit test運(yùn)行的時(shí)候,Robolectric會(huì)自動(dòng)判斷你調(diào)用了Android相關(guān)的代碼textView.getText(),然后這個(gè)調(diào)用過程在底層截取了,轉(zhuǎn)到ShadowTextView的getText實(shí)現(xiàn)。而ShadowTextView是真正實(shí)現(xiàn)了getText這個(gè)方法的,所以這個(gè)過程便可以正常執(zhí)行。
除了實(shí)現(xiàn)Android里面的類的現(xiàn)有接口,Robolectric還做了另外一件事情,極大地方便了unit testing的工作。那就是他們給每個(gè)Shadow類額外增加了很多接口,可以讀取對(duì)應(yīng)的Android類的一些狀態(tài)。比如我們知道ImageView有一個(gè)方法叫setImageResource(resourceId),然而并沒有一個(gè)對(duì)應(yīng)的getter方法叫getImageResourceId(),這樣你是沒有辦法測(cè)試這個(gè)ImageView是不是顯示了你想要的image。而在Robolectric實(shí)現(xiàn)的對(duì)應(yīng)的ShadowImageView里面,則提供了getImageResourceId()這個(gè)接口。你可以用來測(cè)試它是不是正確的顯示了你想要的Image.
下面簡(jiǎn)單的介紹一下使用Robolectric來做unit testing。注意:下面的配置方法指的是AndroidStudio上面的,Eclipse用戶自行g(shù)oogle一下配制方法。
要使用Robolectric,需要做幾步配置工作。
首先需要將它和JUnit4加到你項(xiàng)目的dependencies里面,
testCompile "junit:junit:4.12" testCompile ’org.robolectric:robolectric:3.0-rc3’
其中的Robolectric的最新版本號(hào)可能會(huì)變,具體可以上jcenter查看一下當(dāng)前的最新版本號(hào)。
將Build Variant里面的Test Artifact選擇為Unit Test,如果你找不到Build Variant,可以在菜單欄選擇View -> Tool Windows -> Build Variant. 正常情況下它會(huì)出現(xiàn)在左下角。
如果是Mac的話,還需要配置一個(gè)東西,菜單欄選擇 Run -> Edit Configuration -> Defaults -> JUnit,在Configuration tab將working directory改成$MODULE_DIR$。這個(gè)配置是Robolectric官方文檔提到的,但我用最新的AndroidStudio1.3實(shí)驗(yàn)的時(shí)候,忘了配置這個(gè),貌似也可以正確運(yùn)行,anyway,配置一下也無所謂。具體見Robolectric的官方文檔,最下面那部分。
到這里,就可以開始code了。
測(cè)試代碼是放在app/src/test下面的,test class的位置最好跟target class的位置對(duì)應(yīng),比如MainActivity放在
app/src/main/java/com/domain/appname/MainActivity.java
那么對(duì)應(yīng)的test class MainActivityTest最好放在
app/src/test/java/com/domain/appname/MainActivityTest.java
這里舉個(gè)簡(jiǎn)單又稍微有點(diǎn)用的例子,假設(shè)app里面有兩個(gè)Activity:MainActivity和SecondActivity,MainActivity里面有一個(gè)TextView,點(diǎn)擊一下這個(gè)TextView將跳轉(zhuǎn)到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)); } }); } }
對(duì)應(yīng)的測(cè)試類,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); } }
上面的代碼測(cè)試的就是當(dāng)用戶點(diǎn)擊textView的時(shí)候,程序會(huì)正確的跳轉(zhuǎn)到SecondActivity。其中@RunWith(RobolectricGradleTestRunner.class)表示用Robolectric的TestRunner來跑這些test,這就是為什么Robolectric可以檢測(cè)到你調(diào)用了Android相關(guān)的類,然后截取這些調(diào)用,轉(zhuǎn)到他們的Shadow類的原因。此外,@Config用來配置一些東西。
代碼中的
MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);用來創(chuàng)建MainActivity的instance,或者說,用來啟動(dòng)這個(gè)Activity,當(dāng)Robolectric.setupActivity返回的時(shí)候,這個(gè)Activity已經(jīng)完成了onCreate、onStart、onResume這幾個(gè)生命周期的回調(diào)了。
mainActivity.findViewById(R.id.textView1).performClick();用來觸發(fā)點(diǎn)擊事件。ShadowActivity shadowActivity = Shadows.shadowOf(mainActivity);用來獲取mainActivity對(duì)應(yīng)的ShadowActivity的instance。
shadowActivity.getNextStartedActivity();用來獲取mainActivity調(diào)用的startActivity的intent。這也是正常的Activity類里面不具有的一個(gè)接口。
最后,調(diào)用Assert.assertEquals來assert啟動(dòng)的intent是我們期望的intent。
運(yùn)行這個(gè)unit test,啟動(dòng)命令行,cd到項(xiàng)目的根目錄,運(yùn)行
./gradlew test ,幾秒鐘后,你將看到測(cè)試運(yùn)行的結(jié)果
... :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
在我的機(jī)器上(MacBook Air 2013款,8G內(nèi)存,算比較低的配置),運(yùn)行這個(gè)test只需要不到2秒鐘,這才是TDD該有的速度。
注:第一次運(yùn)行可能需要下載一些library,或者是gradle本身,可能需要花一點(diǎn)時(shí)間,這個(gè)跟unit test本身沒關(guān)。
整個(gè)項(xiàng)目已經(jīng)放到github上面:robolectric-demo
總體來說,Robolectric是個(gè)非常強(qiáng)大好用的unit testing framework。雖然使用的過程中肯定也會(huì)遇到問題,我個(gè)人就遇到不少問題,尤其是跟第三方的library比如Retrofit、ActiveAndroid結(jié)合使用的時(shí)候,會(huì)有不少問題,但瑕不掩瑜,我們依然可以用它完成很大部分的unit testing工作。
有任何意見或建議,或者發(fā)現(xiàn)文中任何問題,歡迎留言評(píng)論!
作者 小創(chuàng) 更多文章 | Github | 公眾號(hào)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/64380.html
摘要:言歸正傳,上一篇文章單元測(cè)試如何開始介紹了幾款單元測(cè)試框架基本用法依賴隔離概念,本篇主要解答單元測(cè)試中幾個(gè)重要問題。在單元測(cè)試交流微信群,很多新進(jìn)來的小伙伴,都會(huì)幾個(gè)大同小異的問題。 showImg(/img/bVEpaD?w=1080&h=715); 原文鏈接:http://www.jianshu.com/p/f5d197a4d83a 前言 已經(jīng)一個(gè)月沒寫文章了,由于9月份在plan...
閱讀 898·2019-08-30 15:54
閱讀 1466·2019-08-30 15:54
閱讀 2400·2019-08-29 16:25
閱讀 1292·2019-08-29 15:24
閱讀 749·2019-08-29 12:11
閱讀 2506·2019-08-26 10:43
閱讀 1227·2019-08-26 10:40
閱讀 466·2019-08-23 16:24