相對 App 的測試方案,市面上已經有非常多且成熟的 UI 級別的自動化測試框架,卻鮮有針對 SDK 提供的自動化測試方案,原因是 SDK 屬于為 App 提供服務的“插件”。一個 App 可接入一到多個 SDK 在內,而在項目中模塊化是非常普遍的架構,所以 SDK 是針對細分功能提供服務的組件,有的提供數據服務、地圖服務或節省開發成本的組件等等,這只能 SDK 開發者根據功能自行完成測試。
本篇說明的 SDK 測試方案是針對數據服務的 SDK 功能覆蓋,皆包含 SDK 的 API、網絡數據及緩存相關的邏輯測試,即非 UI 的純數據邏輯的覆蓋。
本篇是自動化測試基礎上的延伸,相對安卓系統可以便利的通過 adb 指令控制如 App 安裝、卸載、退出應用等“系統”級操作,iOS 在控制 App 層面上只能通過一些間接的手段完成上面幾點需求,為了易于維護,在控制器中以有限狀態機模式進行了構造,以便于后續增加更多的操作狀態和測試用例。
整個測試流程就如下面描述的有向圖,以 Pytest 驅動客戶端執行任務,然后將客戶端輸出的請求數據進行截取處理,而后驗證是否通過測試用例。
Android 可以使用 adb 命令與 app 進行數據上的通信,如發送廣播,啟動 Activity 等。同時也可以使用 shell 命令對配置文件進行修改,再進行 gradle 編譯,實現對 app 級別參數的修改,從而完成不同參數對 app 程序影響的驗證。
iOS 由于系統特性,無法如安卓系統靈活運用系統命令來操作 App 或 SDK,所以以一個 Socket 連接 Server 端進行通信。另外在 iOS 系統上又可利用 Runtime 的特性,將傳輸的字符串轉化為 API 調用,這樣做的好處是將 Socket 模塊和 Runtime 解析模塊編入應用中就無需再次打包,只需 Python 端編好代碼和測試 case,所有的功能調用都由兩端約定的協議解析執行即可。
對于集成 SDK 的 app,如果需要在 App 運行時,觸發一個行為,可以通過廣播來實現??梢愿鶕?action name 完成對行為類型的分類,根據 caseid 完成對行為的區分。如下圖所示:
根據上圖示例如下:
os.system("adb shell am broadcast -a com.umeng.auto.track --es param \"" + str(es) + "\" --ei caseId " + bytes(ei))
其中 com.umeng.auto.track 為廣播的 action name 用以區分類別ei 為一個 int 數,相當于圖中的 caseides 為參數內容,參數的協議可以自由定義,建議使用 json 類型,方便對不同類型的數據進行處理這樣,我們只需要在廣播中以 ei 寫一個 switch 語句,執行不同的行為,如果測試不同參數的效果,可以使用 es 傳遞內容。
如果使用廣播,是沒辦法綁定生命周期,即如果 SDK 需要在 Activity 的 onCreate() 中進行一些類初始化操作,是沒法進行控制的。所以對于這種情況就需要使用 adb 命令中的啟動 Activity 命令,基本流程與廣播類似,但是 caseid 的處理在 onCreate() 中:
根據上圖示例如下:
os.system("adb shell am start -n " + self.pkgname + "/." + activity + " --es param \"" + str(es) + "\" --ei caseId " + bytes(ei))
其中 pkgname 為包名,activity 為 activity 的名字 es 為需要傳入的內容,ei 為一個 int 數,即 caseId。與廣播方式類似,只是將 switch 放到了 onCreate 中,根據 ei 和 es 進行相應的操作。
以上說的兩種方式幾乎可以涵蓋 SDK 測試的部分 case,但是對于部分 SDK,初始化需要在程序一啟動的 Application 中執行,這時上面的兩種方式顯然滿足不了需求。這時有兩套方案可以應對。如下圖所示:
如上圖所示,左邊的部分,我們可以通過修改 Java 文件完成對 Appliction 中內容的修改,如在 Application 中會有一些靜態常量,使用 python 修改 java 文件中的常量,并重新運行:
def changeConstant(self, source,des):
path = os.path.join(os.path.dirname(sys.path[0]), 'autotestAndroid')
gradle_path = os.path.join(path,'app','src','main','java','deep','autotest','utils','Constant.java')
print '-----gradle_path----',gradle_path
if os.path.exists(gradle_path):
build_file = open(gradle_path, 'r+')
lines = build_file.readlines()
for i in range(len(lines)):
line = lines[i]
if ' '+source in line:
arr = line.split('=')
line = arr[0]+ '='+des+";\n"
lines[i] = line
build_file = open(gradle_path, 'w+')
build_file.writelines(lines)
p = buildprocess.CompileProcess(path)
p.start()
else:
print 'nonono='+ gradle_path
除了上述方法,也可以在 Application 中讀取一個 SD 卡配置文件,根據配置文件的協議進行對應的操作。每次只需更改配置文件的內容,并通過 adb push 放入 SD 卡指定路徑中,然后重啟 App 即可。
如「iOS 端測試框架」所見,此時進行通信只有一個應用,這個應用就是我們用來測試 SDK 的 Demo,通過這個宿主我們可以觸發 SDK 提供的任何 API,通過 iOS runtime 我們可以觸發 SDK 的類方法、實例方法甚至是私有 API,但這寫都只局限于一個應用“沙盒”內,如上面說到的安裝、卸載及 App 退出和切到后臺就無能為力了,所以我們引入了另一個 Demo(Watch Demo),通過兩個 Demo 的協同操作滿足“沙盒”之外的需求。
如上面提到的,所有功能調用都基于約定的協議來執行的,協議的設計也是不斷新增的測試需求改造的。
最初 Server 端與客戶端以測試用例的 case id 來區分需要觸發的事件,后來 case id 所代表的含義太多,而且客戶端也是以運行時不斷調用 Server 端發送指令的形式表現執行的具體功能,所以轉為一條執行序列更加靈活及方便擴展。一個測試用例可分為多條執行序列,執行序列內的協議包含了需要進行的方法調用或事件的處理。以 Dplus 為例,如下數據包含了部分操作的執行序列:
"operations": {
"$umeng_cloudayc_op9": {
"arguments": {
"param": [
"$umeng_cloudayc_op*"
]
},
"type": "class",
"class": "DplusMobClick",
"method": "track:"
},
"$umeng_cloudayc_op5": {
"arguments": {
"param": []
},
"next": "$umeng_cloudayc_op9",
"type": "class",
"class": "DplusMobClick",
"method": "clearSuperProperties"
}
},
"type": "invoke",
"description": "401",
"first": "$umeng_cloudayc_op5"
由于是針對 SDK API 測試的協議,所以協議內的格式以調用的類名、方法名及參數為主,再加上部分細節參數加以說明,如 type 是 class 則調用類方法,是 instance 是示例方法。
需要注意的是,這個隊列的結構是個字典,以標識前綴 $umeng_cloudayc_op 作為一個子事件的 key,value 則是其執行參數。而且可以看到在參數 param 的 value 里也有和子事件的 key 類似的值,這里的設計也是為了滿足部分嵌套調用的需求。舉例來說,如此時需要通過一個接口驗證之前緩存的數據是否發送正常,就要分三步,第一存儲數據,第二將數據讀出,第三將第二步的結果作為參數傳入最后調用的接口即可,這樣既能滿足各種嵌套邏輯,又能實現遠程構造客戶端系統的實體對象作為參數進行接口調用。
回到上面的字典的結構,實際上在之前的協議格式使用的是數組作為執行序列的封裝格式,不過在實際應用中無法滿足靈活的要求,就如上面所說的組合的調用邏輯,有部分子事件是被動調用的,通過在其他事件內的參數檢測來觸發調用,如果是數組則無法控制這個執行序列的依賴關系。采用字典后,增加啟動字段,在后續關聯的子事件內,都會說明下一個執行的子事件,如果某個子事件是作為另外子事件的參數,則不會有 next 字段,因為它是被動觸發的,不在執行隊列之內。
在這個業務協議開發過程中,不斷的根據測試需求進行改造、添加,從一開始的單一應用調用接口,到后面的多應用切換、前后臺切換以及應用斷開和重連,需要多套控制流程,在具體實現時,分散到了各個業務邏輯中,每增加一個控制都要兼容考慮是否會影響到其他模塊,而且作為一個自動化測試“框架”,提前梳理好核心部分的流程會讓之后更易于開發和維護,所以就引入了有限狀態機的概念進行構造。
有限狀態機(Finite-state machine)可用于模擬很多事物邏輯,顧名思義,它是一個有限的狀態的處理邏輯,有下面幾個特征:
條件又稱為事件,即當前狀態在滿足這個條件后會觸發一個動作,從而進行狀態裝換動作即在現態滿足條件后需觸發的一系列操作,動作完成后即狀態進行遷移。動作也可以忽略,在某些情況下,現態滿足條件后,也無需執行任何動作就切換到新的狀態。次態是相對現態而言,表示了條件滿足后遷移的狀態,次態也可以與現態相同。根據業務邏輯的特性及復雜程度,合適的使用有限狀態機,可以使得邏輯表達清晰、封裝及維護都很直觀和方便。當一個業務包含的狀態越多,就越適合使用優先狀態機進行封裝處理。有限狀態機應用非常廣泛,如電子電路、編譯器及網絡協議 TCP 協議狀態機等
需要注意的是要區分“動作”和“狀態”,如果將“動作”也視為“狀態”會導致編寫狀態機時產生問題。
將業務邏輯應用到有限狀態機,前提是需要熟悉對應的業務,并將其中的狀態、動作和條件等抽離出來,然后再做進一步的劃分和關聯,構造出一個完整的有向圖。
在自動化測試中,有如下幾個關鍵詞:啟動測試、監聽、主 App 連接、守護 App 連接、接口調用、進入后臺、進入前臺、應用退出、崩潰、斷開連接、重連等。
在日常開發中,如果遇到上面的”事件”,可能就順其自然的開始寫判斷、寫調用,可能不自覺的就寫出了一個“有限狀態機”,不過不會那么嚴格的區分什么是動作什么是狀態,只要滿足最后的結果就能達成目的。但現在我們有意識的利用有限狀態機進行劃分,分離出狀態和動作以及狀態遷移的條件??瓷厦娴年P鍵字,好像都是一個個“動作”,仔細看“監聽 (中)”又可能是一個狀態,但實際上我們還得需要結合業務的理解再抽象出一些狀態,如“進入后臺”,則是跳轉到了守護 App,當前是控制守護 App 的狀態;若是“進入前臺”則守護 App 跳轉到了“主 App”,是控制主 App 的狀態。
如下圖就用剛才抽象出的關鍵詞構造了一個簡單的有限狀態機:
原文轉自:http://www.infoq.com/cn/articles/sdk-test-automation