深入探討Unit Testing in Android
在你開始為Provider寫Case之前,應(yīng)該仔細(xì)讀一讀SDK文檔中關(guān)于Provider測(cè)試的說明。但是光讀那些說明,你還是沒辦法寫出正確的Case,因?yàn)槟阋仓溃珹ndroid的文檔是比較差勁的,有一些關(guān)鍵東西文檔中沒有說明,你也知道,這在Android當(dāng)中并不少見。
你寫個(gè)Provider的Case,如下:
public class DemoProviderTest extends ProviderTestCase2<FeedProvider> {
}
編譯有錯(cuò)誤,它說ProviderTestCase2沒有隱式的構(gòu)造,看來我們需要一個(gè)構(gòu)造函數(shù),寫一個(gè)標(biāo)準(zhǔn)的JUnit構(gòu)造吧!
public class DemoProviderTest extends ProviderTestCase2<FeedProvider> {
public FeedProviderTest(String name) {
super(name);
}
}
WTF,還是有編譯錯(cuò)誤,而且更嚴(yán)重!難道ProviderTestCase2不是繼承自TestCase,用了Eclipse的建議,它創(chuàng)建了一個(gè)帶有二個(gè)參數(shù)的構(gòu)造:
public class DemoProviderTest extends ProviderTestCase2<FeedProvider> {
public FeedProviderTest(String name) {
super(name);
}
public DemoProviderTest(Class<FeedProvider> providerClass,
String providerAuthority) {
super(providerClass, providerAuthority);
// TODO Auto-generated constructor stub
}
}
但是僅一個(gè)名字的FeedProviderTest(String name)還是有錯(cuò)誤,再試試不帶參數(shù)的,還是不行,這說明ProviderTestCase2沒有這樣的構(gòu)造函數(shù),但是沒有道理啊,因?yàn)樗吘故抢^承自TestCase的??!很神奇和詭異啊!
既然ProviderTestCase2沒有一個(gè)參數(shù)的構(gòu)造,那么只能去掉帶有一參數(shù)的構(gòu)造了!
public class DemoProviderTest extends ProviderTestCase2<FeedProvider> {
public DemoProviderTest(Class<FeedProvider> providerClass,
String providerAuthority) {
super(providerClass, providerAuthority);
}
public void testConstructor() throws Throwable {
assertNotNull("can construct resolver", getMockContentResolver());
ContentProvider provider = getProvider();
assertNotNull("can instantiate provider", provider);
}
}
寫了一個(gè)基本的測(cè)試,運(yùn)行了下,得到了一個(gè)Warning,是由JUnit Framework報(bào)出來的說DemoProviderTest沒有定義公共的構(gòu)造函數(shù)TestCase(name)或TestCase(),什么情況,不是我不定義而是有編譯錯(cuò)誤啊,因?yàn)樵撍赖腜roviderTestCase2沒有這二個(gè)構(gòu)造!該死,只能再把這個(gè)構(gòu)造加回來!但是因?yàn)楦割悰]有,只能引用父類的雙參數(shù)的構(gòu)造了!
public class DemoProviderTest extends ProviderTestCase2<FeedProvider> {
public DemoProviderTest() {
super(null, null);
}
public DemoProviderTest(Class<FeedProvider> providerClass,
String providerAuthority) {
super(providerClass, providerAuthority);
}
public void testConstructor() throws Throwable {
assertNotNull("can construct resolver", getMockContentResolver());
ContentProvider provider = getProvider();
assertNotNull("can instantiate provider", provider);
}
}
但是參數(shù)傳什么呢?先用Null試試中吧!完全有錯(cuò)誤,在父類的構(gòu)造初始化時(shí)出現(xiàn)了NPE,這說明傳Null肯定是不對(duì)的!看了下強(qiáng)加的帶有二個(gè)參數(shù)的構(gòu)造DemoProviderTest(Class<FeedProvider> providerClass, String providerAuthority),也說應(yīng)該傳一個(gè)Class對(duì)象,和Provider的Authority,再試試看!
public class DemoProviderTest extends ProviderTestCase2<FeedProvider> {
public DemoProviderTest() {
super(FeedProvider.class, AUTHORITY);
}
public DemoProviderTest(Class<FeedProvider> providerClass,
String providerAuthority) {
super(providerClass, providerAuthority);
}
public void testConstructor() throws Throwable {
assertNotNull("can construct resolver", getMockContentResolver());
ContentProvider provider = getProvider();
assertNotNull("can instantiate provider", provider);
}
}
這次Okay了,但是這樣一來二個(gè)參數(shù)的構(gòu)造就沒有意義了,于是讓一個(gè)參數(shù)的調(diào)用二個(gè)參數(shù)的:
public DemoProviderTest() {
this(FeedProvider.class, AUTHORITY);
}
還是Okay,這說明我們的Case必須給ProviderTestCase2提供正確的構(gòu)造參數(shù)!
再加上setUp和tearDown:
@Override
public void setUp() throws Exception {
mContentResolver = getMockContentResolver();
}
@Override
public void tearDown() throws Exception {
mContentResolver = null;
}
運(yùn)行,發(fā)現(xiàn)testConstructor掛了,說getMockContentResolver()返回的是Null,這怎么可能啊,太詭異了!想到還是可能初始化未正確,給setUp加上了父類的調(diào)用:
@Override
public void setUp() throws Exception {
super.setUp();
mContentResolver = getMockContentResolver();
}
@Override
public void tearDown() throws Exception {
super.tearDown();
mContentResolver = null;
}
這下再跑,全都Okay了,說明凡是涉及到重寫(Override)父類的方法,都要調(diào)用父類的方法,以期正確初始化!下面是正確的完整版:
public class DemoProviderTest extends ProviderTestCase2<FeedProvider> {
private ContentResolver mContentResolver;
public DemoProviderTest() {
this(FeedProvider.class, AUTHORITY);
}
public DemoProviderTest(Class<FeedProvider> providerClass,
String providerAuthority) {
super(providerClass, providerAuthority);
}
@Override
public void setUp() throws Exception {
super.setUp();
mContentResolver = getMockContentResolver();
}
@Override
public void tearDown() throws Exception {
super.tearDown();
mContentResolver = null;
}
public void testConstructor() throws Throwable {
assertNotNull("can construct resolver", getMockContentResolver());
ContentProvider provider = getProvider();
assertNotNull("can instantiate provider", provider);
}
}
總結(jié)一下,從這個(gè)例子得到的經(jīng)驗(yàn)是,對(duì)于組件的測(cè)試,都要繼承自android.test.*下面的組件測(cè)試框架,但是需要給這些組件測(cè)試框架傳遞正確的參數(shù),否則Case無法測(cè)試:
二個(gè)構(gòu)造函數(shù)
public DemoProviderTest() {
this(FeedProvider.class, AUTHORITY);
}
public DemoProviderTest(Class<FeedProvider> providerClass,
String providerAuthority) {
super(providerClass, providerAuthority);
}
一個(gè)都不能少,而且是JUnit的指定構(gòu)造函數(shù)(帶有一個(gè)String,或不帶參數(shù)的)要調(diào)用測(cè)試架構(gòu)指定的構(gòu)造,以給測(cè)試框架傳遞正確的參數(shù)!
還有就是重寫的父類方法時(shí),一定要把父類的方法也調(diào)用上,否則還是不會(huì)初始化正確!
但是這里不得不說這些組件測(cè)試框架寫的真是不好用,首先,那個(gè)名字就讓人費(fèi)解,為什么有個(gè)2?。ndroid真夠2的!還有,既然作為框架,應(yīng)該把初始化的工作做完整,做徹底,這樣才能稱的上框架。使用者應(yīng)該只需要繼承,把自己的事情做完,就應(yīng)該能進(jìn)行工作,就像組件Activity或ContentProvider一樣,到了你的代碼里的時(shí)候,框架里的初始化工作已經(jīng)做完,所以你,繼承者只需要關(guān)心你自已的初始化工作就好!但是測(cè)試框架就爛,繼承者不但要關(guān)心自己的初始化還要保證給父類傳遞正確的參數(shù)!
2. Testing for Activity
同樣對(duì)于Activity的測(cè)試也是要注意初始化的部分,只不過對(duì)于setUp和tearDown你不調(diào)super也沒有關(guān)系!
public class ExplorerActivityTester extends
ActivityInstrumentationTestCase2<ExplorerActivity> {
public ExplorerActivityTester() {
this(TARGET_PACKAGE_NAME, ExplorerActivity.class);
}
public ExplorerActivityTester(String pkg, Class<ExplorerActivity> class1) {
super(pkg, class1);
}
@Override
public void setUp() {
mInstrumentation = getInstrumentation();
}
}
3. Obstacles to unit testing
在Android里面,由于其系統(tǒng)架構(gòu)的特性決定了給Android寫單元測(cè)試用例和驗(yàn)證測(cè)試用例特別因難
a. Activity reuse
原因就是每一個(gè)測(cè)試的包,測(cè)試的包也是一個(gè)Apk,每一個(gè)包只能注入一個(gè)目標(biāo)Apk,也就是說只能針對(duì)一個(gè)Apk里面的內(nèi)容進(jìn)行測(cè)試,一旦某個(gè)操作跳到了Apk以外的地方,就超出了測(cè)試框架的控制范圍。但是組件重用機(jī)制在Android中非常的普遍,通過Intent來跳到其他的應(yīng)用(apk)中,調(diào)用其他應(yīng)用的組件來完成某個(gè)操作,這是Android的特性,是再普遍不過的了!但這就給單元測(cè)試用例埋下了無法逾越的障礙。測(cè)試框架本身更弱,一但跳出了某個(gè)組件,Instrumentation便無法對(duì)其進(jìn)行控制,開源測(cè)試框架robutium-solo一定程度上解決了這個(gè)問,Solo可以操作一個(gè)包內(nèi)的任何組件,特別地它能夠解決多個(gè)Activity跳轉(zhuǎn)的問題,但是如前所述,因?yàn)橐粋€(gè)測(cè)試Apk只能注入一個(gè)目標(biāo)Apk,所以一旦Activity跳到了應(yīng)用外,Solo也沒有了辦法。這是一個(gè)無解的問題。因此,Android當(dāng)中做測(cè)試,只能關(guān)注一些邏輯層,API層,數(shù)據(jù)和Provider,Service等一些與表層操作較遠(yuǎn)的代碼!對(duì)于表層Activity跳來跳去的情況,只能做部分測(cè)試,或用MockObject來解決,但是這通常失去了測(cè)試的本身意義,因?yàn)橐ù罅繒r(shí)間去創(chuàng)建MockObject,不值!
b. ActionBar is not clickable
還有一非常惡心的問題是,對(duì)于Activity的ActionBar無法直接點(diǎn)擊,真的不明白Google到底在搞什么,弄出來個(gè)新東東,竟然測(cè)試框架里面不支持操作!想到點(diǎn)擊ActionBar只能通過Solo來點(diǎn)擊屏幕坐標(biāo),這非常難以移植和維護(hù)!
說到操作,還不得不說原生框架Instrumentation支持的操作非常少,而且不好用,它只能派發(fā)KeyEvent事件,很多情況下都不好用,比如有個(gè)對(duì)話框,想要點(diǎn)擊Okay或是Cancel的話,就很麻煩,再如想點(diǎn)擊一個(gè)ListView中的某一項(xiàng)的話也是非常麻煩!同樣第三方的robotium-solo框架就好用多了,它進(jìn)行了很好的封裝,通過Solo.clickOnText()就可以方便的點(diǎn)擊屏幕上的帶有此文字的View。它的內(nèi)部實(shí)現(xiàn)方式是通過View的顯示Tree,根據(jù)Tag(文字)來查找相關(guān)的View,然后對(duì)其發(fā)送點(diǎn)擊事件!這也解釋了為什么Solo也無法點(diǎn)擊ActionBar,因?yàn)锳ctionBar不是在Activity的View中,它是像StatusBar一樣,屬于系統(tǒng)級(jí)別的東西!
c. StatusBar belongs to Settings.apk
難以想象吧,隨處可見的Statusbar竟然以屬于Settings,只有注入了Settings的包才能對(duì)Statusbar進(jìn)行操作。所以雖然Statusbar上面有你的Apk的相關(guān)的東西(比如提示)但是你還是無法直接操作它,除非你寫一個(gè)專門注入Settings.apk的測(cè)試包!
4. Security Concern
測(cè)試的代碼(Instrumentation和TestRunner)也是以一個(gè)Apk的形式存在的,它可注入任何目標(biāo)Apk,然后就可以對(duì)其進(jìn)行操作,甚至獲取其資源和數(shù)據(jù)。這就帶來了安全上面的問題!可以把一個(gè)帶有測(cè)試代碼的Apk當(dāng)成一個(gè)應(yīng)用,一旦在某個(gè)手機(jī)運(yùn)行,但可以操作任何一個(gè)應(yīng)用。
其實(shí),這本來不是問題,如果應(yīng)用市場(chǎng)能對(duì)開發(fā)者上傳的應(yīng)用進(jìn)行嚴(yán)格的測(cè)試和審核。但是現(xiàn)在的問題是無論是Google Play還是其他市場(chǎng)都不怎么測(cè)試,所以就會(huì)讓不良者有機(jī)可乘!
其實(shí),這里的關(guān)鍵問題在于,Android廠商不要盲目的追求數(shù)量!把應(yīng)用集中銷售是Apple想出來的主意,Apple的App Store也是做的最好的!Android只是一個(gè)效仿者,所以你發(fā)展的慢,數(shù)量不多,質(zhì)量不夠,收入不好,是正常的,因?yàn)槟闶且粋€(gè)追隨者,你起步晚!對(duì)于廠商來講,數(shù)量你沒有辦法控制,無法一下子弄出幾萬個(gè)應(yīng)用來,這個(gè)是需要時(shí)間的,但是,至少,你可以嚴(yán)格控制質(zhì)量啊!你可以做到對(duì)上傳的應(yīng)用進(jìn)行嚴(yán)格的測(cè)試,這是對(duì)用戶負(fù)責(zé),也是對(duì)自己負(fù)責(zé)啊!所以無論是設(shè)備還是應(yīng)用程序,都是Apple的要優(yōu)質(zhì)一些,Android總是要?dú)埓我恍?,所以你看Apple的東西價(jià)格就高,Android就便宜,當(dāng)然價(jià)格也是Android的唯一優(yōu)勢(shì)!現(xiàn)的社會(huì)是一分錢一分貨,便宜自然就沒好貨!
相關(guān)文章
Android TraceView和Lint使用詳解及性能優(yōu)化
這篇文章主要介紹了Android TraceView和Lint使用詳解及性能優(yōu)化的相關(guān)資料,需要的朋友可以參考下2017-03-03Android compose氣泡升起和水滴下墜動(dòng)畫實(shí)現(xiàn)示例
這篇文章主要為大家介紹了Android compose氣泡升起和水滴下墜動(dòng)畫實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01android開發(fā)socket編程之udp發(fā)送實(shí)例分析
這篇文章主要介紹了android開發(fā)socket編程之udp發(fā)送,實(shí)例分析了Android開發(fā)socket網(wǎng)絡(luò)編程中udp發(fā)送的相關(guān)技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-04-04android 實(shí)現(xiàn)控件左右或上下抖動(dòng)教程
這篇文章主要介紹了android 實(shí)現(xiàn)控件左右或上下抖動(dòng)教程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-03-03Android中Spinner控件之鍵值對(duì)用法實(shí)例分析
這篇文章主要介紹了Android中Spinner控件之鍵值對(duì)用法,實(shí)例分析了Spinner控件控件的鍵值對(duì)實(shí)用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-09-09Android連接服務(wù)器端的Socket的實(shí)例代碼
這篇文章主要介紹了Android連接服務(wù)器端的Socket的實(shí)例代碼,需要的朋友可以參考下2017-05-05android換膚功能 如何動(dòng)態(tài)獲取控件中背景圖片的資源id?
這篇文章主要為大家詳細(xì)介紹了android換膚功能中如何動(dòng)態(tài)獲取控件中背景圖片的資源id? ,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-08-08從"Show?tabs"了解Android?Input系統(tǒng)
這篇文章主要介紹了從"Show?tabs"了解Android?Input系統(tǒng)的相關(guān)資料,需要的朋友可以參考下2023-01-01Glide實(shí)現(xiàn)加載圖片顯示進(jìn)度條效果
Glide作為安卓開發(fā)常用的圖片加載庫,有許多實(shí)用而且強(qiáng)大的功能,那么,下面這篇文章主要給大家介紹了利用Glide實(shí)現(xiàn)加載圖片顯示進(jìn)度條效果的相關(guān)資料,文中給出了詳細(xì)的示例代碼供大家參考學(xué)習(xí),需要的朋友們下來一起看看吧。2017-05-05淺談Android為RecyclerView增加監(jiān)聽以及數(shù)據(jù)混亂的小坑
下面小編就為大家?guī)硪黄獪\談Android為RecyclerView增加監(jiān)聽以及數(shù)據(jù)混亂的小坑。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-04-04