spoon+robotium+jenkins進行自動化持續迴歸測試

自動化測試的意義
        別說是外行人,即使是正在從事自動化測試工作的人來說,現在或曾經都或多或少有過這樣的疑惑,辛苦寫了自動化測試用例,卻基本發現不了問題,其意義何在?在說明這個意義前先看下質量的定義。

質量的定義:
        維基百科中對於品質(Quality)的定義:中國大陸亦稱爲“質量”,可指物品的特徵、品性、本質,也可指商品或服務的水準、質量。
影響品質的要素包括物品的可靠性、安全性,功能上是否完備,能否滿足需求, 等等。
        對於軟件質量的定義:軟件質量,是指軟件系統或系統中的軟件部分的質量,即滿足用戶需求,包括功能需求和性能需求的程度。軟件的質量包括功能、性能、可靠性、安全性、可升級性、可維護性、其他質量特性,這也是我們一般認識中的質量,從而也指導着大多數的質量相關的測試工作
        從以上的定義中也可以看出,對於質量,大家的認知更多的都是在“質”上,而較少地考慮到了“量”上,質量質量應該是質與量的結合,既關注“質”即品質保證,也要關注“量”即效率。隨着互聯網的高速發展,特別是隨着移動互聯網的到來,企業希望能夠更快地響應市場變化且又希望產品的品質也能夠有所保障,因此各類軟件項目提速提質相關的如敏捷、持續集成、持續交付、自動化等等技術就誕生了。也就是說對於質量,我們的目的不再僅僅是品質,而是真正的“質量”,即Move fast and don’t break things!
        知曉了質量的本質與目的後,再來看前面的問題就好解釋多了,自動化測試較手工測試來說,確實遠沒有手工測試更能發現問題,但正如前文所述質量的意義除了在於“質”外,還有“量”,因此,自動化測試更大的意義在於“量”上,在於提高效率上,且實際項目中,自動化也不僅僅只是把測試自動化,但凡能提高效率且適合自動化的均應該進行自動化。當然了,理論上自動化測試還是應該要能發現20%左右的問題的,對於一點問題都發現不了的自動化測試,則需要好好思考測試用例設計、測試框架搭建方面的問題了。如何更好、更快、更穩定、更可靠地進行測試自動化,這也即是本文所要分享的。在進入正題前,先看看質量的意義。

質量的意義:
對於用戶,質量更多的就意味着軟件品質:
        試想這麼一個例子:某天,大明正在夢鄉中與吃着美味的烤鴨,突然被震耳的手機鬧鈴吵醒了,大明前一晚由於知道明天早上有重要會議,所以特地將手機鬧鈴調早了半個小時,但也正因爲如此,手機鬧鈴軟件出了點故障,比原定時間遲了半小時才響。。大明只好匆匆忙忙顧不上早餐就出門趕去上班了,且趕緊用打車軟件叫了輛車,大明心想應該還來得急,就耐心地等着出租車的到來,在瑟瑟寒風轉眼間就過去10分鐘了,大明開始急了,出租車怎麼還不來。。又過了10分鐘,出租車終於出現,原來新手出租車司機由於不認識大明的住處,因此使用了車上的導航,結果由於導航系統未及時更新地圖數據,司機開往了錯誤的地方。。意料之中的,因爲沒能及時參加會議,大明捱了一頓批。快到中午了,由於心情不佳不想出去吃鈑了,大明用外賣軟件叫了個外賣,已經飢腸轆轆的大明塞上耳機聽着音樂希望可以放鬆些,結果音樂中間莫名中斷過好幾次,心情反而更糟了。。。至於下午的事就不說了,最終大明度過了鬱悶的一天。
        雖然以上的大明略顯悲慘,但也決不是沒可能發生的,隨着移動互聯網的到來,每個人的生活越來越需要手機及其上的軟件,每個人的幸福度很大程度地受各種軟件的影響,如果所有的軟件都能很好地運行,這個世界將變得多麼美好~

對於企業,質量則將意味着真正的“質量”:
        隨着信息的不斷爆炸式增長,我們這個時代的節奏也越來越快,特別是移動互聯網,一個軟件的項目週期再也不能像傳統軟件那樣半年甚至好幾年。試想,如果你的軟件項目的持續交付能力比競爭對手弱,那麼將意味着對方可以更快地響應市場變化、可以比你更快地將創意付諸實現,那麼很顯然地,你將失去市場,且在移動互聯網,在這種情況下往往花費巨大的營銷運營代價都無法挽回頹勢。因此,提高軟件項目的整體效率勢在必行,且需要時時刻刻地去思考如何才能加快項目的運轉速度、提高持續交付產品的能力並且還能更有效地保障產品的質量。
        “讓天下人都能用上更好的軟件”,想必這應該是質量、測試人員,甚至應該是所有軟件相關從業者的任務與使命~
        基於以上背景結合實際實踐,轉入《spoon+robotium+jenkins進行自動化持續迴歸測試》正題。。

自動化測試的原因:
        例如敏捷測試中所提到的,手動測試需要太長的時間、手動過程容易出錯、自動化讓人們有時間做更有價值的工作、自動化的迴歸測試提供了安全網、自動化測試能較早且頻繁地提供反饋、測試提供文檔等等。
        簡而言之,自動化測試可以提高測試效率,將重複的活動自動化後可以有更多的時間去做更多有創造性、更需要人腦思考的探索性測試上。
        基於UI自動化測試的必要性:對於軟件,即使編寫了單元測試用例、組件測試用例,但這些測試並不是將軟件組合成一個整體進行集成測試的,因此很難發現最接近於用戶使用的集成測試問題,況且,很多項目團隊壓根就沒有單元測試。對於android來說,系統版本碎片化較爲嚴重,編寫基於UI的自動化測試用例,還可以用來進行兼容性測試。

app自動化測試應該包含以下幾個基本能力:
多機並行執行:同時在多臺手機中執行自動化測試用例,以便在不同版本的Android操作系統中進行迴歸測試、兼容性測試
出錯重試及出錯截圖:當用例執行未通過時,可以自動進行重跑並截圖記錄,以便減少因偶然因素導致用例執行未通過的情況
實時日誌記錄:對於測試執行過程應該能記錄運行時的日誌,以便詳細發解測試執行情況
跨應用的能力:能夠測試包含跨應用的諸多情況
生成Junit形式測試報告:生成詳細的Junit形式的測試報告,可方便查看測試用例執行結果
代碼覆蓋率報告:生成代碼覆蓋率報告,以便進一步指導測試策略
持續快速反饋的能力:對於測試運行情況,應該要能夠快速反饋
易於訪問的報告:能夠很方便地訪問到測試報告詳情

spoon介紹
項目地址:https://github.com/square/spoon
該項目的主要目的在於將能夠將基於instrumentation的測試用例分發到各個不同的手機上,執行並將測試結果收集起來,生成最終的HTML總結報告。
將項目下載下來後,進入spoon/website/sample目錄,訪問index.html即可看到示例如下:

點擊查看後有截圖展示功能、運行時日誌展示功能:


生成報告的目錄結構如下:

其中junit-reports收集有junit格式的xml報告,通過jenkins的junit插件可以很方便生成單元測試報告
簡單執行命令如下:

java -jar spoon-runner-1.1.1-jar-with-dependencies.jar \
    --apk example-app.apk \
    --test-apk example-tests.apk

詳細參數:

Options:
    --apk              Application APK
    --fail-on-failure  Non-zero exit code on failure
    --output            Output path
    --sdk              Path to Android SDK
    --test-apk          Test application APK
    --title            Execution title
    --class-name        Test class name to run (fully-qualified)
    --method-name      Test method name to run (must also use --class-name)
    --no-animations    Disable animated gif generation
    --size              Only run test methods annotated by testSize (small, medium, large)
    --adb-timeout      Set maximum execution time per test in seconds (10min default)

注:
1.由於spoon報告中的靜態頁面中使用的是googleapis中的在線字體,因此報告可能打開會相當慢
        <link href="http://fonts.googleapis.com/css?family=Roboto:regular,medium,thin,italic,mediumitalic,bold" rel="stylesheet">
因此實際要做爲報告時,建議翻牆後訪問http://fonts.googleapis.com/cssfamily=Roboto:regular,medium,thin,italic,mediumitalic,bold,將字體文件下載下來保存至文件例如放至static/fonts.css,然後靜態引用<link href="static/fonts.css" rel="stylesheet">

2.spoon中未提供開啓代碼覆蓋率的Option
        spoon由於只是封裝了am instrument命令,最終執行的其實還是am instrument執行用例的命令,因此是可以開啓代碼覆蓋率功能的,要想增加-e coverage true選項使其支持收集代碼覆蓋率,則需要修改spoon源碼,最方便的那就是在源碼裏寫死選項了
SpoonDeviceRunner.java中增加如下兩行寫死參數:
    	      logDebug(debug, "About to actually run tests for [%s]", serial);
    	      junitReport = FileUtils.getFile(output, JUNIT_DIR, serial + "_" + i + "_" +".xml");
    	      runner = new RemoteAndroidTestRunner(testPackage, testRunner, device);
    	      runner.setCoverage(true);//注:set爲true後,被測應用打包時需要插樁才能生成ec代碼覆蓋率統計文件
    	      runner.addInstrumentationArg("coverageFile", "/sdcard/robotium/spoon" + i + ".ec");
    	      runner.setMaxtimeToOutputResponse(adbTimeout);
這樣,當測試完成後,就會在sd卡中生成emma用於覆蓋率統計的ec文件,將ec文件pull到CI服務器上
java -cp /usr/local/program/emma/emma.jar emma report -sp $source_path -r xml -in coverage.em,$ec_files
生成相應的xml文件後,結合jenkins中的相應插件就可以生成覆蓋率報告了。


3.spoon不能將測試用例集分開執行

adb shell am instrument -w -e class com.android.foo.FooTest,com.android.foo.TooTest com.android.foo/android.test.InstrumentationTestRunner 

        如上,我們知道通過am instrument執行用例時,可以指定多個用例集,當被測試應用啓動後,所有接下來要執行的用例集都運行在同一個進程中,如果在執行FooTest時,被測應用crash了,那麼TooTest 將不再執行,你所收集到的測試結果也就不含TooTest的結果了。當你的用例集較多時,顯然希望每次迴歸測試時能儘量把所有用例均執行過,而不希望因前面一個用例crash導致後面所有用例都沒有執行到。
這時分開執行將類似如下:
adb shell am instrument -w -e class com.android.foo.FooTest com.android.foo/android.test.InstrumentationTestRunner
adb shell am instrument -w -e class com.android.foo.TooTest com.android.foo/android.test.InstrumentationTestRunner
想要讓spoon支持這種方式運行只能修改spoon源碼了
      對於以上幾點不好的地方,博主fork了源碼做了部分修改,請見:https://github.com/hunterno4/spoon
        至此,通過spoon即可完成多機並行執行實時日誌記錄生成Junit形式測試報告、收集代碼覆蓋率報告等功能了

出錯重試及出錯截圖
        對於自動化測試用例,如果只是執行一次,常常因爲偶然因素導致用例不能通過,如果每次收到的測試報告都是因爲測試用例執行問題,顯然這樣的報告很快就不會有人願意看了。
出錯重試一般性做法:
1.收集測試執行時的控制檯輸出,然後分析輸出是否包含某些指定關鍵字來判斷是否失敗了,然後進行重跑,這種方式是比較繁瑣的。
2.根據Android junit自帶的@FlakyTest實現的機制,重寫runTest
重寫runTest()方法可以參考http://qa.baidu.com/blog/?p=985
注:
1) 重寫runTest時,需要注意的是,測試用例的執行流程如下:
setUp()——> runTest()——> tearDown()
這裏的重跑只是重複執行test*()方法,因此對於許多跨Activity的測試用例,例如從Activity A跳轉至Activity B,但用例在Activity B中斷言失敗了,此時重跑時,會繼續執行test***()方法中點擊跳轉到B的那塊代碼,但此時界面還處於B中,顯然是找不到A中的那些控件的,因此在重寫runTest()方法時,需要在super.runTest()前自動地跳轉到最初的Activity。這個可以通過solo.goBackToActivity()完成
2)在失敗截圖時,我們也常常不只希望就只在斷言出錯時截那一張圖,而是希望能截取一系列運行過程的圖,這可以通過solo.startScreenshotSequence()方法完成
實現方式如下:
	@Override
    protected void runTest() throws Throwable {
		
    	String testMethodName = getName();
    	String currentTestClass = getClass().getName();
    	LogUtils.logD(TAG, "currentTestClass:" + currentTestClass);
    	boolean isScreenShot = true;
    	boolean isScreenShotWhenPass = false;
    	long startTime = 0;
    	long endTime = 0;
    	Holo holo = new Holo(getInstrumentation(), getActivity());//這裏的holo是經過增刪後的robotium,理解爲solo即可
    	String currentActivity = getActivity().getClass().getSimpleName();
    	LogUtils.logD(TAG, "currentActivity:" + currentActivity);
    	Method method = getClass().getMethod(getName(), (Class[]) null);           
    	
    	int retrytime = 3;
    	if (method.isAnnotationPresent(RetryTest.class)) {
    		retrytime = method.getAnnotation(RetryTest.class).retrytime();
    		isScreenShot = method.getAnnotation(RetryTest.class).isScreenShot();
        } 
    	LogUtils.logD(TAG, "isScreenShot:" + isScreenShot);
    	
    	int runCount = 0;
    	
    	do {
    		LogUtils.logD(TAG, "runCount:" + runCount);
    		try {
    			holo.goBackToActivity(currentActivity);
    			if(runCount > 0){//當用例第一遍執行未通過後,開啓截圖序列,至於要截多少張圖,可以根據實際情況來設計
    				holo.stopScreenshotSequence();
    				holo.startScreenshotSequence(endTime, 5, testMethodName, currentTestClass);
    			}
    			startTime = SystemClock.uptimeMillis();
    			super.runTest();
    			endTime = SystemClock.uptimeMillis() - startTime;
    			LogUtils.logD(TAG, "run test" + testMethodName + ",testcase pass with time cost:" + endTime);
    			if(isScreenShotWhenPass){
    				holo.takeSpoonScreenShot(testMethodName,currentTestClass,testMethodName,DEFAULT_QUALITY);
    			}
    			if(holo != null){
					holo = null;
				}
    			
    			break;				
			} catch (Throwable e) {				
				if(retrytime>1 && runCount<retrytime-1){
					runCount++;
					endTime = SystemClock.uptimeMillis() - startTime;
					LogUtils.logD(TAG, "run test" + testMethodName + ",testcase failed with time cost:" + endTime);
					continue;					
				}else {
					if(isScreenShot){
						LogUtils.logD(TAG, "takeScreenshot:");
						holo.takeSpoonScreenShot(testMethodName,currentTestClass,testMethodName,DEFAULT_QUALITY);
					}	
					if(holo != null){
						holo = null;
					}
					
					throw e;
				}
				
			}
    					
		} while (runCount < retrytime);
    	
    }
另外,spoon本身的截圖client是不支持截圖序列的,因此可以結合robotium本身的截圖序列的功能,這個只要保證截圖的目錄是spoon runner能理解的即可。

跨應用的能力
        對於基於instrumentation的框架來說,跨應用能力是其弱勢,要想跨應用而又不借助服務端的話,那基本是得root手機了。
不過隨着android系統版本的不斷提高,android4.3及以上很快也就會成爲主流,而4.3後基於Uiautomation就可以從容跨應用了。
另外,對於基於instrumentation的框架,我們的測試工程還可以自由調用android的api來完成robotium所不具備的功能,例如完成短信攔截、網絡切換、解鎖與鎖屏等。
以切換網絡爲例:
        當我們實現相關的功能時,可能需要添加用戶敏感的權限,如:
 <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
而這樣的權限可能在我們的被測應用中是沒有的,且也不希望在被測應用中加上這樣的權限,因此我們只能加到自己的測試工程中。
由於被測應用沒有相應的權限,而我們的測試用例是與被測應用運行在同一進程中的,因此要想切換網絡的話如果直接調用相關的api顯然是會報權限問題的,這時我們可以通過把相關的方法封裝,然後在測試用例中通過廣播的形式來調用。而我們在自己的測試工程裏註冊接收器,當接收到相應的廣播時則調用切換網絡的api,這樣的話切換網絡相關的api則是運行在測試工程這個進程中,而這個進程是有相關權限的。
        簡而言之,當我們把測試工程.apk與被測工程.apk安裝到手機上時,我們既可以控制被測工程.apk這個進程,也可以控制測試工程.apk這個進程。而在測試工程.apk這個進程上,我們可以做絕大多數普通apk能做的事情,甚至於能做普通apk不能做的事情。
        例如,我們可以在測試工程.apk上寫個Activity,然後還可以在測試工程.apk中寫個testcase去測試這個Activity。我們還可以在被測應用自動運行時,在測試工程中啓個service監控cpu、內存、流量消耗等性能指標。我們可以默默地連接網絡、發送http請求,以後通過Uiautomation甚至於無需root都可以去切換設置。我們幾乎無所不能,權大,就是任性。。。

持續快速反饋的能力
        測試完成後可以通過郵件反饋,對於jenkins的話,可以通過Email-ext plugin擴展郵件功能。這個插件也提供很多了郵件模版,可以直接在郵件裏展示例如svn變更集、junit報告詳情等。在選擇要採用的郵件模版前可以先測試下:

注:在jenkins中要使用郵件模版的話,需要將插件自帶的模版拷貝至你jenkins的workspace目錄下的email-template(默認不存在,需要自己創建)目錄下

易於訪問的報告
        通過Spoon執行後的測試結果默認是收集到了spoon-output這個目錄下,且可以通過index.html靜態頁面進行訪問,因此要想方便地訪問每次的測試結果的話,部署個例如tomcat這樣的Web容器就可以了。不過考慮到維護方面且spoon-output是靜態的,那就完全沒必要再弄個容器了,jenkins有個userContent目錄,放於這個目錄下的所有文件都可以通過jenkins直接訪問。
因此我們可以把每次測試完成後生成的結果文件自動copy到userContent下,且按照每次構建的id來存放。
例如:
http://192.168.10.111:8080/jenkins/userContent/spoon/162/spoon-output/index.html
這樣在每次測試完成後,收到反饋郵件時就可以直接通過郵件中的鏈接訪問測試結果了。

總結:
        至此,spoon+robotium+jenkins進行自動化持續迴歸測試就可以滿足基本的核心需求了,當項目每天check in有新代碼時,我們通過這些自動化測試用例在多臺手機上進行迴歸測試,測試完成後收到郵件,若有問題時,訪問測試結果詳情頁,根據截圖與當時的運行時日誌判斷是否有bug,若有的話,即時反饋進行bug修復。這樣的話,我們即可在多臺手機上同時完成迴歸測試與兼容性測試,手機越多效益越大。當然了,自動化持續迴歸測試只是提升“質”與“量”的一部分,對於測試團隊來說,接口測試自動化、測試環境運維自動化、性能監控等等等等還有很多。如何提升項目的效率這是永恆的主題,而對於傳統的更多地只關注品質的測試團隊來說,在新時代下也將越來越難以滿足企業對效率的追求,想必這也是google工程生產力團隊之所以誕生的本質動因。
        願景:對於軟件業來說,也許目前的自動化測試仍然還是比較初級的,但正如萊特兄弟發明飛機時,只能飛很小的一段距離,但經過多年幾代人不斷的付出與努力後,飛行工具已經給人類帶來了巨大的效益,即使往更近了看,製造業的自動化已經將衆多產業的的流水線變成了自動化,從而大大提高了製造工業的生產力,因此,相信軟件業的自動化也將極大地提高軟件項目的生產力。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章