1. 程式人生 > >Appium自動化之框架搭建

Appium自動化之框架搭建

必要性

    每一次軟體釋出新版本的時候,新的功能模組可能與舊的功能模組產生衝突,而導致原來的功能出現Bug,所以每次發版前都要做一次迴歸測試以保證原來的功能可以正常使用,而每次的迴歸測試都產生了重複的勞動力。為保證軟體相容性,每次的測試都需要在不同的平臺上進行測試,而當前手機等Android裝置五法八門,各種牌子,各種型號,所以往往要在多款手機上進行相同的測試來保證相容性。顯然,這些大量的重複勞動力由自動化測試來完成就很有必要了。當下最炙手可熱的自動化框架非Appium莫屬了,由於它具有跨平臺性、多語言支援和技術社群活躍等優點,所以我們選擇了它來搭建自動化框架。

易用性

    既然要搭建一套自動化測試框架,那麼就必須做到完全自動化,儘可能減少人工操作,提高易用性,完全自動化包括

①,自動採集裝置資訊,無需手動獲取,當USB口接入一臺新裝置可直接開啟自動化測試工作;

②,自動配置資訊,無需手動配置,摒棄TestNG群控時需要手工配置多個suite.xml的方式;

③,自動啟動Appium服務,無需手動開啟,由自動化工作開始的時候通過程式碼開啟;

④,自動安裝新版本軟體,無需手動安裝,由自動化工作開始前通過對比版本進行軟體更新。

    接下來針對以上4點的實現流程做簡述:

①,在做Appium自動化之前,我們是需要配置裝置名和版本,往往我們通過adb指令來獲取,然後填上去,雖然也是簡單的兩句指令,但是每次接入一個新手機都要獲取一次也是有點麻煩,所以為什麼我們不讓程式碼來獲取並自動獲取呢?我們知道程式碼是可以直接執行adb指令的,所以我們只要在每次執行測試前先執行我們的獲取裝置資訊的程式碼,那麼就不用手動再敲了。具體實現如下,分別為獲取裝置名和根據裝置名獲取版本。

public static List<String> getDevices(String adb) {
    List<String> devices = new ArrayList<>();
List<String> results = RuntimeUtil.exec(adb + " devices");
    if (results.size() > 0) {
        for (int i = 0; i < results.size(); i++) {
            String deviceName = results.get(i);
deviceName = deviceName.substring(0, deviceName.indexOf("\t")); devices.add(deviceName); } } else { new Throwable("Can't find devices").printStackTrace(); } return devices; } public static String getPlatformVersion(String adb,String deviceName) { List<String> results = RuntimeUtil.exec(adb + " -s " + deviceName + " shell getprop ro.build.version.release"); return results.get(0); }

②,獲取完配置資訊,就需要把資訊設定進去了,以往我們是通過填在一個xml配置檔案裡的:

<suite name="Suit1">
    <parameter name="port" value="4723" />
    <parameter name="bootstrap_port" value="4724" />
    <parameter name="chromedriver_port" value="9515" />
    <parameter name="udid" value="Q505T" />
    <parameter name="node" value="node" />
    <parameter name="appiumMainJs" value="C:/Users/dell1/AppData/Local/Programs/appium-desktop/resources/app/node_modules/appium/build/lib/main.js" />
    <parameter name="platformName" value="Android" />
    <parameter name="platformVersion" value="4.3" />
    <parameter name="deviceName" value="Q505T" />
    <parameter name="appPackage" value="com.tencent.mobileqq" />
    <parameter name="appActivity" value="com.tencent.mobileqq.activity.SplashActivity" />
    <test name="testqq">
        <classes>
            <class name="testqq.TestMessage" />
        </classes>
    </test>
</suite>

單獨的一個配置檔案,很直觀也方便,但是需要手動配置,那麼有沒可能手動生成這個檔案呢,當然是可以的,但是生成一個檔案是通過java操作io流的方式,而這種方式是比較耗時的,所以還有沒其他方式呢,想到這個xml配置檔案最終也是會被解析成一個物件的,那麼我們直接根據資訊構造出一個物件不就可以了嗎,而且,通過檢視程式碼,也發現了testNG是支援構造物件的形式來配置的,通過setXmlSuites方法可配置一個XmlSuite列表進去。

接下來我們看看如何配置一個XmlSuite,通過xml檔案的配置方式和testNG的原始碼,大致理清其結構如下


當然class裡還有最小的測試單元method。以上元素屬性缺一不可,基本上與Xml配置檔案相似,比較坑的一點就是XmlTest必須指定其所屬的XmlSuite,否則會報空,這點在配置檔案裡是看不出來的,需要根據testNG原始碼才能找出來。既然知道了XmlSuite物件結構,那麼接下來就可以編寫構造程式碼了,這裡我編寫一個XmlSuiteBuilder來構造:

public class XmlSuiteBuilder {

    List<XmlTest> mTests = new ArrayList<>();
Map<String, String> parameters = new HashMap<>();
XmlSuite xmlSuite = new XmlSuite();
    public XmlSuiteBuilder(int index, String deviceName, String platformVersion, Configure configure) {
        parameters.put("port",(4723+2*index)+"");
parameters.put("bootstrap_port",(4724+2*index)+"");
parameters.put("chromedriver_port",(9515+index)+"");
parameters.put("udid",deviceName);
parameters.put("platformName","Android");
parameters.put("platformVersion",platformVersion);
parameters.put("deviceName",deviceName);
parameters.put("node", configure.getNode());
parameters.put("appiumMainJs", configure.getAppiumMainJs());
parameters.put("appPackage", configure.getAppPackage());
parameters.put("appActivity", configure.getAppActivity());
parameters.put("app",configure.getApkPath());
parseTestBeans(configure.getTestBeans());
xmlSuite.setName(deviceName);
xmlSuite.setTests(mTests);
}
    private void parseTestBeans(List<TestBean> testBeans){
        for(TestBean testBean : testBeans){
            String testName = testBean.getName();
Class[] testClasses = testBean.getClasses();
List<XmlClass> xmlClassListt = new ArrayList<>();
            for (Class testClass : testClasses) {
                xmlClassListt.add(new XmlClass(testClass));
}
            XmlTest xmlTest = new XmlTest();
xmlTest.setName(testName);
xmlTest.setClasses(xmlClassListt);
xmlTest.setXmlSuite(xmlSuite);
mTests.add(xmlTest);
}
    }

    public XmlSuite build() {
        xmlSuite.setParameters(parameters);
        return xmlSuite;
}
}

構造好XmlSuite後就可以通過setXmlSuites給testNG設定進去了。

③,通過程式碼執行命令列來啟動Appium,而不是點選桌面圖示的方式:

if(!RuntimeUtil.isProcessRunning("0.0.0.0:"+port)) {
    new Thread(new Runnable() {
        @Override
public void run() {
            String cmd = nodePath + " \"" + appiumPath + "\" " + "--session-override " + " -p "
+ port + " -bp " + bootstrapPort + " --chromedriver-port " + chromeDriverPort + " -U " + udid;
RuntimeUtil.exec(cmd);
}
    }).start();
SleepUtil.s(10000);
    while(!RuntimeUtil.isProcessRunning("0.0.0.0:"+port)){
        SleepUtil.s(2000);
}
}

④,自動安裝新版本Apk的思路是,首先設定好Apk路徑,每次安裝前通過AXMLPrinter2檢測路徑下的Apk的版本,然後再檢測手機中安裝好的Apk的版本,通過對比版本來判斷是否要更新。更新通過adb install指令即可。這裡本想通過adb shell dumpsys package 這條指令來獲取手機中Apk的版本號,但是發現只有部分手機才能獲取成功,所以得換一種方式,那是什麼方式呢?做過安卓開發的都知道安卓應用是可以用PackageManager 通過包名獲取到Apk的資訊的,所以這裡我的想法就是往手機上安裝一個工具Apk,然後通過這個Apk來取到目標Apk的資訊,然後寫入檔案中,最後adb pull把檔案複製到電腦解析出來,這樣子獲取出來的不僅僅只有版本號,還有一些其他有價值的資訊,這些資訊是通過adb獲取不了的。

程式碼分模組、分層

    我們應用基本上都是按模組來劃分的,比如登入模組,訊息模組,充值模組等,所以我們的用例一般也是按照模組來編寫測試,同理,對應自動化測試上面也是根據模組來劃分的,每個模組可能有上百條用例,上百條用例不可能寫在一個類裡,所以需要再對模組進行一次劃分子模組,其對應關係如下:

    XmlTest   →   測試模組   →   包

    XmlClass  →   測試模組的子模組    →   包裡面的類

    method    →   子模組中的case    →   類裡的所有方法

    程式碼分層主要分為元素定位層和邏輯操作層,我們平時的操作可能會把元素定位和邏輯操作寫在一個方法裡,這樣當一個頁面複雜的話會導致這兩種程式碼混在一起,不易檢視和維護,所以我們得做好程式碼分層,這裡我們把元素定位剝離出來,採用如下註解的方式來實現元素定位。

@FindBy(id = "com.tencent.mobileqq:id/ivTitleBtnRightText")
WebElement element;

遇到比較複雜的,比如查詢TabWidget裡的所有FrameLayout,依然可以用註解的方式查詢

@FindBys({@FindBy(className = "android.widget.TabWidget"),@FindBy(className = "android.widget.FrameLayout")})
List<WebElement> list;

自動化效能測試

    以往在對App的效能測試的時候,往往使用一些現成的測試工具,比如GT,Emmagee,Mat等,然後手工操作一遍功能流程,從而生成報告進行分析,然後這種方式有幾個侷限:
    ①,測試過程依賴於手工;
    ②,測試結果只能看到整體效能走勢,如果出現效能佔用的高峰,則不能確定具體是哪些操作或者哪個頁面導致的;
    ③,因為這些工具大多采用的是RunTime來執行命令獲取被測應用的效能資料,比如top命令獲取pid、訪問/proc命令,而Google爸爸已經在7.0以上的系統禁用了這些命令,所以這些工具在非Root的情況下只支援7.0以下;
    ④,只能測試cpu,記憶體,流量,電量等,無法深入到應用裡檢測並收集記憶體洩漏日誌,卡頓日誌,crash日誌,並註明是哪些操作或者哪個頁面造成的;
    而現在,我們可以做到突破以上四個侷限:
    針對①,我們在自動化測試功能的同時,同時也進行著自動化的效能測試,這樣就可以拋棄手工測試效能的方式;
    針對②,在測試同時每採集一次效能資料就記錄當前執行的是哪個case,這樣出現效能佔用高峰的時候可以知道是哪個case造成的;
    針對③,這裡我們採用adb來獲取效能資料,包括cpu,記憶體,流量資料。adb目前的許可權還是很高的,不會存在7.0以上不支援的情況;
    針對④,編寫測試應用,測試應用整合記憶體洩漏檢測,卡頓檢測,crash檢測,採用Instrument方式使得測試應用和被測應用跑在同一個程序,然後開啟記憶體洩漏檢測,卡頓檢測,crash檢測,出現以上問題則把相應的日誌資訊傳送回主機,主機接收到就獲取當前正在執行case,並在最終的報告中體現出執行什麼操作,出現了什麼異常,具體是在哪行程式碼造成的

    為了實現相容不同的待測應用,需要對測試應用Apk進行動態修改,考慮到每次重新編譯比較麻煩,這裡通過逆向修改。

報告定製

    最終測試完成需要生成一份報告,報告所展示的資訊要求直觀明瞭,清晰自然,所以這裡使用了體驗更好的ReportNG,並對其做了定製修改。
    ①,新增case的執行時間;
    ②,新增失敗截圖,截圖點選放大;
    ③,新增case作者資訊,case失敗了,需要查詢原因方便找到責任人;
    ④,新增失敗重跑策略,其中沒有開啟網路不重跑,crash、anr不重跑直接採集crash或anr日誌輸出到報告,其他情況執行重跑,重跑的case還是失敗了會被testNG標記為多個skipedTest,需要進行去重,去重後把最後一個skipedTest的runCount賦值給passedTest或failedTest,最終顯示在報告上是passedTest或failedTest的執行次數,而沒有skipedTest;
    ⑤,新增圖表資訊,包括測試結果圓餅圖,和測試過程中應用的CPU,記憶體和流量的曲線圖;
    ⑥,本地化,新增多一份reportng.properties_zh_rCN,需要注意的是裡面的中文需要轉成ascll碼,否則打出來的包會亂碼。

    最終生成的報告如下圖,當然後面新增功能還要進一步完善



展望未來

    雖然框架已經做了大部分的優化工作,但是還存在一些不足,以及需要優化的地方
    ①,框架暫未相容iOS,但由於Appium本身相容iOS,所以未來要實現也是有據可循的;

    ②,實現通過用例的匯入,就可以自動生成程式碼去實現自動化測試。例如在一份Excel測試用例裡,每條用例增加一項自動化指令碼,而這些程式碼可以通過指令碼錄製生成程式碼來寫入,然後匯入這份用例,框架可以收集用例資訊自動生成測試類,最終實現無需編寫程式碼即可進行自動化測試;

    ③,未來考慮相容更多優秀的外掛,實現外掛的可拔插,比如接入自動遍歷外掛AppCrawler,安全檢測外掛,介面Hook外掛等;

    ④,ReportNG報告的定製需要html,js,css,velocity等前端技術,由於我沒有系統的學習過前端,目前這一塊我基本上是查一點改一點,可謂舉步維艱,未來有時間考慮系統的學習一下這些技術吧。

    總之,一句話,任重而道遠。