基於Appium的UI自動化測試

爲何須要UI自動化測試

移動端APP是一個複雜的系統,不一樣功能之間耦合性很強,很難僅經過單元測試保障總體功能。UI測試是移動應用開發中重要的一環,可是執行速度較慢,有不少重複工做量,爲了減小這些工做負擔,提升工做效率,須要引入可持續集成的自動化測試方案。java

爲何選擇Appium

Appium是一款開源測試工具,能夠用來測試安卓/iOS/Windows端的原生應用和Web混合應用。node

  1. 爲了應對快速迭代的移動端應用功能,愈來愈多的App採用混合模式,即將部分功能交給應用內嵌的Web頁面實現。Appium能方便的切換測試原生應用或App內嵌的web頁面,對於Hybrid App有很好的支持。android

  2. Appium使用各個平臺自身提供的測試框架,所以無需引入第三方代碼或從新打包應用。web

    平臺 測試框架
    Android 4.2+ UiAutomator/UiAutomator2(默認)
    Android 2.3+ Instrumentation(由Selendroid提供)
    iOS 9.3 以上 XCUITest
    iOS 9.3 如下 UIAutomation
  3. Appium在GitHub上開源,維護頻率很高,社區也有相對較高的活躍度。在社區的不斷努力下,Appium能始終保持兼容最新版本的手機操做系統和官方提供的測試框架,功能也愈來愈完善,包括基本的log收集、錄屏、基於opencv的圖像識別等,以及最近版本添加的iOS 13/Android 10支持等;面試

  4. Appium支持經過自定義插件尋找元素,GitHub上也有第三方在開發可用插件,例如基於人工智能的icon識別控件示例工程;也能夠自定義插件,使用圖像識別、OCR等方式查找頁面元素。正則表達式

使用Cucumber組織case

Appium支持多種編程語言,包括Java、Python等,可是直接使用代碼維護case可閱讀性較差,學習成本也比較高,引入Cucumber可使用更接近天然語言的方式組織case。 Cucumber是支持BDD(Behaviour-Driven Development,行爲驅動開發)的工具,能夠自定義語法規則模版,將文本描述的步驟轉爲使用代碼執行的步驟。 因爲Cucumber和Java 8均兼容中文文本編碼,所以能夠自定義中文操做步驟,比起英文代碼更易於理解。以定義一個最基本的點擊操做爲例,預期的語法規則爲"當 點擊 [元素名稱]",則可使用以下定義:npm

   // Cucumber使用正則表達式匹配引號中的內容做爲type參數
   @當("^點擊 \"([^\"]*)\"$")
   public void findElementAndClick(String type) throws Throwable {
       // driver爲Appium對待測設備的抽象,全部測試步驟最終轉爲對driver對操做
       // type能夠傳入元素ID對應的字符串,By.id表示經過元素resource-id查找
       driver.findElement(By.id(type)).click();
   }

編寫case時,使用UI自動化測試經常使用的Page Object設計模式,即爲APP中須要測試的UI頁面定義一個Page對象,該對象中包含頁面上的可操做或可校驗元素,並添加經常使用方法。編程

以花椒首頁爲例,能夠新建一個名爲"首頁"的對象,該對象中包含"搜索"、"個人"、"開播"等元素對應的查找方式(例如搜索按鈕,對應可用來查找元素的resource-id爲com.huajiao:id/main_home_top_search)。因爲在搜索頁輸入用戶uid進行搜索是一個經常使用操做,能夠爲此定義一個"搜索"方法。 全部測試用例、Page對象、元素、方法都使用測試後臺網頁進行保存和編輯,而且實現了基本關鍵詞補全功能。設計模式

如上定義基本的點擊、滑動、輸入文本等操做,創建好適當的頁面和方法後,一條用例就能轉化爲與天然相近的case描述(#開頭行爲註釋行):緩存

# "$首頁.搜索"表示使用"首頁"Page中的"搜索"元素
當 點擊 $首頁.搜索
# "$搜索.搜索()"表示調用搜索頁面的搜索方法,括號內爲搜索關鍵詞參數
$搜索.搜索(43011080)
當 斷言元素出現 $搜索.搜索結果

編寫代碼進行復雜的自定義操做

經過Cucumber定義經常使用操做,如點擊、滑動、校驗文本等,能夠下降編寫一條測試用例的工做量,提升測試用例可讀性,但並不是全部功能均可以使用經常使用操做的方式。尤爲是由於Cucumber只支持一步一步順序執行指令,沒法進行分支或循環指令,所以複雜的操做邏輯須要在自定義步驟中編寫代碼完成操做。 編寫代碼部分封裝參考Android官方提供的Espresso工程,經過鏈式調用的方式進行"查找-操做-校驗"的流程。

以Android客戶端退出登錄爲例,點擊底部"首頁-個人"元素,若當前爲未登陸狀態,則會彈出登錄彈出,此時底部"首頁-個人"元素不可見,說明已是未登陸狀態。

因爲Cucumber順序執行,沒法進行"個人"元素可見時退出登錄,不可見時關閉登錄彈窗,所以須要編寫代碼自定義退出登錄步驟:

  @當("^退出登陸$")
    public void logout() throws Throwable {
        // 點擊"首頁-個人"
        onView(By.id("com.huajiao:id/bottom_tab_user")).perform(click());
        try {
            // 若是當前用戶已登錄,不會彈窗提示登錄,"首頁-個人"元素可見
            onView(By.id("com.huajiao:id/bottom_tab_user")).check(matches(isDisplayed()));
            // 調用退出登陸的方法
            logOut();
        }
        // 未登陸狀態,"首頁-個人"元素不存在,拋出NoSuchElementException
        catch (NoSuchElementException e) {
            // 點擊系統back鍵關閉登錄彈窗
            onActions().pressKeyCode(AndroidKey.BACK, "1").perform();
        }
    }

使用Appium查找UI元素

  1. 基本查找方式

    • By.id: 經過元素的resource-id進行查找;
    • MobileBy.AndroidUIAutomator(String code): 經過UIAutomator2的代碼文本查找。code爲符合UIAutomator2規範的代碼文本,Appium會解析文本後使用反射的方式調用UIAutomator2進行查找;以下爲使用UiSelector查找文本包含text的元素: String code = "new UiSelector().textContains(\"" + text + "\");";
  2. xpath查找元素
    xpath能夠用來在XML文檔中查找元素和屬性。Appium和谷歌官方提供的uiautomatorviewer工具獲取元素都是xml形式組織的,xpath能夠精準定位僅靠By.idBy.className沒法定位的元素:

    • 文案是"TEXT"元素的兄弟元素,該兄弟元素的resource-id是"ID":
      xpath://*[@text='TEXT')]/../android.widget.TextView[@resource-id='ID']
    • resource-id是"ID"且選中狀態元素的子元素,該子元素的attr屬性爲value: xpath://*[@resource-id='ID' and @selected='true']/*[@attr='value']

    雖然xpath方式查找元素更精準,可是元素的路徑可能受到佈局改動的影響,且在iOS上性能不佳,所以推薦優先使用resource-id等方式組合定位元素

  3. 圖像識別查找元素
    Appium在By Selector級別支持按照圖片查找By by = MobileBy.image(base64ImageString)。目前不支持多元素查找,只返回第一個查找到的元素。
    讓Appium支持圖片查找,須要一點前期準備工做:

    1. 安裝NodeJS版本的OpenCV庫:npm install -g opencv4nodejs

    2. Appium中配置相關參數:

      // 設置圖片識別閾值,默認0.4。須要嘗試在找不到元素和找到不匹配元素間的平衡
      driver.setSetting(Setting.IMAGE_MATCH_THRESHOLD, 0.5);
      // 圖片識別耗時較長,能夠在操做元素對時候再也不次查找圖片,以節省時間
      driver.setSetting(Setting.CHECK_IMAGE_ELEMENT_STALENESS, false);

StaleElementReferenceException: Appium查找到元素,以後嘗試操做元素時,若元素已經不在當前頁面DOM資源上時會拋出StaleElementReferenceException異常。 Appium使用UIAutomator2查找元素時,會保留元素的緩存,對元素進行操做時,會直接把緩存的信息交給UIAutomator2進行點擊、滑動等操做。

  • 實際測試過程當中,可能出現步驟:A頁面跳轉B頁面;在B頁面點擊元素el。而A、B兩個頁面都有與el相同ID的元素,在B頁面上嘗試操做元素el的時候,Appium直接使用了A頁面的緩存,此時會出現StaleElementReferenceException
  • 因爲Appium採用HTTP請求查找和操做元素,所以查找元素和操做元素實際流程是:POST查找元素->server緩存元素->POST操做緩存的元素,有時間間隔。在網絡請求期間若是出現APP端彈窗等元素遮擋,也可能致使StaleElementReferenceException

總體工做流程

  1. htest client客戶端獲取打包安卓打包服務器下載列表,從中篩選出最新的APK安裝包版本。若是有高於手機端的最新版本,則覆蓋安裝手機端花椒APP,並自動觸發BVT測試用例執行(執行單個case時直接從測試平臺網頁端觸發);
  2. 測試平臺選出Cucumber描述的BVT用例集,同時查找Page頁面,轉義用例步驟的元素和方法,替換爲客戶端可以使用的元素定位符(id:開頭表示經過resource-id查找,text:開頭表示經過文本內容查找),經過HTTP請求返回給客戶端(執行單個case時使用socket方式發送)。
  3. 執行測試用例過程當中,可能在查找元素時剛好遇到手機端彈窗蓋住花椒APP元素等狀況,所以在執行測試用例過程當中,會檢測手機端可能出現的、非測試步驟中預期的彈窗,包括首充彈窗、開播禮物下載彈窗等,關閉彈窗後再次查找元素;


  4. htest client初始化Appium driver,以Appium做爲代理鏈接手機,並在手機端執行測試用例中的基本操做;
  5. 若是執行測試用例失敗,會嘗試從新執行失敗的用例,若是再次失敗,會收集手機端日誌、保存截圖和錄屏,並將失敗日誌返回保存到測試平臺中, 執行單個case時使用socket發送執行結果, 結果經過htest Server回傳給測試平臺進行展現, 若是bvt時,則經過接口回傳結果數據

使用測試平臺網頁端單次執行測試用例:

若是對軟件測試、接口測試、自動化測試、面試經驗交流。感興趣能夠加軟件測試交流:1085991341,還會有同行一塊兒技術交流。
按模塊劃分,整個框架分爲:

    1. 測試平臺: 網頁端,用於保存、編輯基於Cucumber的測試用例,管理Page頁面,解析用例中的元素,將轉義後的用例發送給客戶端,展現客戶端實際執行結果;

    2. htest server: Java中間件,使用的netty框架, 負責轉發socket消息,即測試平臺通知客戶端執行用例消息,和客戶端執行結果返回測試平臺。 使用:

      • 在htest中server端netty的啓動com.htest.server.server.BaseServer

        @Overridepublic void run() {
            if (bossGroup == null) {
                bossGroup = new NioEventLoopGroup();
                model.setBossGroup(bossGroup);
            }
            if (workerGroup == null) {
                workerGroup = new NioEventLoopGroup();
                model.setWorkGroup(workerGroup);
            }
            ServerBootstrap b = new ServerBootstrap(); 
            b.group(model.getBossGroup(),model.getWorkGroup())
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 100)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .handler(new LoggingHandler(LogLevel.INFO))
                .childHandler(getChildHandler());
            try {
                future = b.bind(SERVER_IP, getPort()).sync(); 
                LOGGER.debug("服務啓動成功 ip={},port={}",SERVER_IP, getPort());
                future.channel().closeFuture().sync();
            } catch (Exception e) {
                LOGGER.error("Exception{}", e);
            } finally {
                Runtime.getRuntime().addShutdownHook(new Thread() {
                    @Override public void run() {
                        shutdown();
                    }
                });
            }
        }
      • HttpServer、JarServer、WebsocketServer都是相同的啓動方式,區別在於他們監聽的端口不一樣,處理數據的handler不一樣

      • HttpServer的處理器是com.htest.server.handler.ServerHttpHandler,處理消息是按照http協議處理的

        @Override
        protected void messageReceived(ChannelHandlerContext ctx, HttpRequest request) {
            try {
                this.request = request; headers = request.headers();
                if (request.method() == HttpMethod.GET) {
                    QueryStringDecoder queryDecoder = new 
                        QueryStringDecoder(request.uri(), Charset.forName("utf-8")); 
                    Map<String, List<String>> uriAttributes = queryDecoder.parameters(); //此處僅打印請求參數(你能夠根據業務需求自定義處理) 
                    for (Map.Entry<String, List<String>> attr : uriAttributes.entrySet()){
                        for (String attrVal : attr.getValue()) {
                            Logs.HTTP.debug(attr.getKey() + "=" + attrVal);
                        }
                    }
                }
                if (request.method() == HttpMethod.POST) {
                    fullRequest = (FullHttpRequest) request;
                    //根據不一樣的 Content_Type 處理 body 數據
                    dealWithContentType();
                }
                keepAlive = HttpHeaderUtil.isKeepAlive(request);
                writeResponse(ctx.channel(), HttpResponseStatus.OK, "開始執行", keepAlive);
            } catch (Exception e) {
                writeResponse(ctx.channel(), HttpResponseStatus.INTERNAL_SERVER_ERROR, "啓動失敗", true);
            }
        }
        JarServer的處理器是,處理消息是按照protobuf格式處理的com.htest.server.handler.ServerHandler
      • @Override
        protected void handleData(ChannelHandlerContext ctx, MessageModel.Message msg) {
            Connection connection = server.getConnectionManager().get(ctx.channel());
            connection.updateLastReadTime();
            server.getMessageReceiver().onReceive(msg, connection);
        }
      • WebsocketServer的處理器是com.htest.server.handler.ServerChannelHandler,它也是按照protobuf格式處理消息的,跟HttpServer不一樣之處在於他們的ChannelInitializer不一樣

    3. htest client: Java客戶端,用於定義Cucumber步驟,更新手機APK,初始化Appium,執行測試用例; 使用方式:在pc端命令行中執行java -jar htest-client.jar,pc端須要有Appium和nodejs opencv環境,經過yaml配置文件控制執行測試過程當中端參數。 具體工做方式以下:

      • 功能:該jar支持定時檢查最新apk功能,默認是不開啓的,經過yaml文件配置是否開啓。若是發現有最新apk,會自動安裝到手機,並給web服務器(管理自動化case的測試平臺)發送一次請求,觸發一次指定模塊case集執行。
      • 下載策略:該系統默認只下載最新的apk,若是本地yaml配置文件中的apkVersion值比服務器上的apkVersion值。若是比服務器的小,則不下載。
      • 安裝策略:下載完成後首先會比對手機中的apk的versionName(經過aapt解析出來的)與下載的apk的versionName大小,若是下載的apk新,則進行安裝,不然不安裝。也能夠配置參數安裝到指定的手機,若是隻有一臺手機則不用配置參數。
      • 安裝完成後會自動更新apkVersion的值,用於下次的判斷。
      • 安裝完成後會向web服務器發送http請求,web服務器收到後會觸發一次,派發給當前手機case集任務,具體case集模塊由models參數配置,結果郵件接收人經過mails配置。
    4. Appium: NodeJS客戶/服務端,用於鏈接手機,經過UIAutomator2/XCUITest,在手機端執行獲取元素/點擊/滑動等基本操做;以上內容就是本篇的所有內容以上內容但願對你有幫助,有被幫助到的朋友歡迎點贊,評論。

相關文章
相關標籤/搜索