1. Testing for ContentProvider
在你開始為Provider寫Case之前,應該仔細讀一讀SDK文檔中關於Provider測試的說明。但是光讀那些說明,你還是沒辦法寫出正確的Case,因為你也知道,Android的文檔是比較差勁的,有一些關鍵東西文檔中沒有說明,你也知道,這在Android當中並不少見。
你寫個Provider的Case,如下:
復制代碼 代碼如下:
public class DemoProviderTest extends ProviderTestCase2<FeedProvider> {
}
編譯有錯誤,它說ProviderTestCase2沒有隱式的構造,看來我們需要一個構造函數,寫一個標准的JUnit構造吧!
復制代碼 代碼如下:
public class DemoProviderTest extends ProviderTestCase2<FeedProvider> {
public FeedProviderTest(String name) {
super(name);
}
}
WTF,還是有編譯錯誤,而且更嚴重!難道ProviderTestCase2不是繼承自TestCase,用了Eclipse的建議,它創建了一個帶有二個參數的構造:
復制代碼 代碼如下:
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
}
}
但是僅一個名字的FeedProviderTest(String name)還是有錯誤,再試試不帶參數的,還是不行,這說明ProviderTestCase2沒有這樣的構造函數,但是沒有道理啊,因為它畢竟是繼承自TestCase的啊!很神奇和詭異啊!
既然ProviderTestCase2沒有一個參數的構造,那麼只能去掉帶有一參數的構造了!
復制代碼 代碼如下:
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);
}
}
寫了一個基本的測試,運行了下,得到了一個Warning,是由JUnit Framework報出來的說DemoProviderTest沒有定義公共的構造函數TestCase(name)或TestCase(),什麼情況,不是我不定義而是有編譯錯誤啊,因為該死的ProviderTestCase2沒有這二個構造!該死,只能再把這個構造加回來!但是因為父類沒有,只能引用父類的雙參數的構造了!
復制代碼 代碼如下:
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);
}
}
但是參數傳什麼呢?先用Null試試中吧!完全有錯誤,在父類的構造初始化時出現了NPE,這說明傳Null肯定是不對的!看了下強加的帶有二個參數的構造DemoProviderTest(Class<FeedProvider> providerClass, String providerAuthority),也說應該傳一個Class對象,和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了,但是這樣一來二個參數的構造就沒有意義了,於是讓一個參數的調用二個參數的:
復制代碼 代碼如下:
public DemoProviderTest() {
this(FeedProvider.class, AUTHORITY);
}
還是Okay,這說明我們的Case必須給ProviderTestCase2提供正確的構造參數!
再加上setUp和tearDown:
復制代碼 代碼如下:
@Override
public void setUp() throws Exception {
mContentResolver = getMockContentResolver();
}
@Override
public void tearDown() throws Exception {
mContentResolver = null;
}
運行,發現testConstructor掛了,說getMockContentResolver()返回的是Null,這怎麼可能啊,太詭異了!想到還是可能初始化未正確,給setUp加上了父類的調用:
復制代碼 代碼如下:
@Override
public void setUp() throws Exception {
super.setUp();
mContentResolver = getMockContentResolver();
}
@Override
public void tearDown() throws Exception {
super.tearDown();
mContentResolver = null;
}
這下再跑,全都Okay了,說明凡是涉及到重寫(Override)父類的方法,都要調用父類的方法,以期正確初始化!下面是正確的完整版:
復制代碼 代碼如下:
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);
}
}
總結一下,從這個例子得到的經驗是,對於組件的測試,都要繼承自android.test.*下面的組件測試框架,但是需要給這些組件測試框架傳遞正確的參數,否則Case無法測試:
二個構造函數
復制代碼 代碼如下:
public DemoProviderTest() {
this(FeedProvider.class, AUTHORITY);
}
public DemoProviderTest(Class<FeedProvider> providerClass,
String providerAuthority) {
super(providerClass, providerAuthority);
}
一個都不能少,而且是JUnit的指定構造函數(帶有一個String,或不帶參數的)要調用測試架構指定的構造,以給測試框架傳遞正確的參數!
還有就是重寫的父類方法時,一定要把父類的方法也調用上,否則還是不會初始化正確!
但是這裡不得不說這些組件測試框架寫的真是不好用,首先,那個名字就讓人費解,為什麼有個2啊!Android真夠2的!還有,既然作為框架,應該把初始化的工作做完整,做徹底,這樣才能稱的上框架。使用者應該只需要繼承,把自己的事情做完,就應該能進行工作,就像組件Activity或ContentProvider一樣,到了你的代碼裡的時候,框架裡的初始化工作已經做完,所以你,繼承者只需要關心你自已的初始化工作就好!但是測試框架就爛,繼承者不但要關心自己的初始化還要保證給父類傳遞正確的參數!
2. Testing for Activity
同樣對於Activity的測試也是要注意初始化的部分,只不過對於setUp和tearDown你不調super也沒有關系!
復制代碼 代碼如下:
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裡面,由於其系統架構的特性決定了給Android寫單元測試用例和驗證測試用例特別因難
a. Activity reuse
原因就是每一個測試的包,測試的包也是一個Apk,每一個包只能注入一個目標Apk,也就是說只能針對一個Apk裡面的內容進行測試,一旦某個操作跳到了Apk以外的地方,就超出了測試框架的控制范圍。但是組件重用機制在Android中非常的普遍,通過Intent來跳到其他的應用(apk)中,調用其他應用的組件來完成某個操作,這是Android的特性,是再普遍不過的了!但這就給單元測試用例埋下了無法逾越的障礙。測試框架本身更弱,一但跳出了某個組件,Instrumentation便無法對其進行控制,開源測試框架robutium-solo一定程度上解決了這個問,Solo可以操作一個包內的任何組件,特別地它能夠解決多個Activity跳轉的問題,但是如前所述,因為一個測試Apk只能注入一個目標Apk,所以一旦Activity跳到了應用外,Solo也沒有了辦法。這是一個無解的問題。因此,Android當中做測試,只能關注一些邏輯層,API層,數據和Provider,Service等一些與表層操作較遠的代碼!對於表層Activity跳來跳去的情況,只能做部分測試,或用MockObject來解決,但是這通常失去了測試的本身意義,因為要花大量時間去創建MockObject,不值!
b. ActionBar is not clickable
還有一非常惡心的問題是,對於Activity的ActionBar無法直接點擊,真的不明白Google到底在搞什麼,弄出來個新東東,竟然測試框架裡面不支持操作!想到點擊ActionBar只能通過Solo來點擊屏幕坐標,這非常難以移植和維護!
說到操作,還不得不說原生框架Instrumentation支持的操作非常少,而且不好用,它只能派發KeyEvent事件,很多情況下都不好用,比如有個對話框,想要點擊Okay或是Cancel的話,就很麻煩,再如想點擊一個ListView中的某一項的話也是非常麻煩!同樣第三方的robotium-solo框架就好用多了,它進行了很好的封裝,通過Solo.clickOnText()就可以方便的點擊屏幕上的帶有此文字的View。它的內部實現方式是通過View的顯示Tree,根據Tag(文字)來查找相關的View,然後對其發送點擊事件!這也解釋了為什麼Solo也無法點擊ActionBar,因為ActionBar不是在Activity的View中,它是像StatusBar一樣,屬於系統級別的東西!
c. StatusBar belongs to Settings.apk
難以想象吧,隨處可見的Statusbar竟然以屬於Settings,只有注入了Settings的包才能對Statusbar進行操作。所以雖然Statusbar上面有你的Apk的相關的東西(比如提示)但是你還是無法直接操作它,除非你寫一個專門注入Settings.apk的測試包!
4. Security Concern
測試的代碼(Instrumentation和TestRunner)也是以一個Apk的形式存在的,它可注入任何目標Apk,然後就可以對其進行操作,甚至獲取其資源和數據。這就帶來了安全上面的問題!可以把一個帶有測試代碼的Apk當成一個應用,一旦在某個手機運行,但可以操作任何一個應用。
其實,這本來不是問題,如果應用市場能對開發者上傳的應用進行嚴格的測試和審核。但是現在的問題是無論是Google Play還是其他市場都不怎麼測試,所以就會讓不良者有機可乘!
其實,這裡的關鍵問題在於,Android廠商不要盲目的追求數量!把應用集中銷售是Apple想出來的主意,Apple的App Store也是做的最好的!Android只是一個效仿者,所以你發展的慢,數量不多,質量不夠,收入不好,是正常的,因為你是一個追隨者,你起步晚!對於廠商來講,數量你沒有辦法控制,無法一下子弄出幾萬個應用來,這個是需要時間的,但是,至少,你可以嚴格控制質量啊!你可以做到對上傳的應用進行嚴格的測試,這是對用戶負責,也是對自己負責啊!所以無論是設備還是應用程序,都是Apple的要優質一些,Android總是要殘次一些,所以你看Apple的東西價格就高,Android就便宜,當然價格也是Android的唯一優勢!現的社會是一分錢一分貨,便宜自然就沒好貨!