解決 App 自動化測試的常見痛點

霍格沃兹测试开发学社發表於2024-03-27

App 自動化測試中有些常見痛點問題,如果框架不能很好的處理,就可能出現元素定位超時找不到的情況,自動化也就被打斷終止了。很容易打消做自動化的熱情,導致從入門到放棄。比如下面的兩個問題:

一是 App 啟動載入時間較久(可能 App 本身載入慢,可能移動裝置本身載入應用速度慢,也可能首頁廣告時間較長)。

另一個是各種彈框的出現,廣告彈框,升級彈框,評價彈框等。在框架中如果不能處理好上面的情況,

以雪球 App 出現的幾種彈框舉例:

彈框一:

彈框二:

彈框三:

  • 彈框的影響範圍

  • 彈框對我們自動化的影響主要是用例執行的打斷,而至於彈框中廣告內容的跳轉或評價資訊填寫等屬於另外的測試,因此我們主要是要將彈框處理消失,使應用回到用例執行的 PO;

  • 彈框的消失方式

  • 觀察彈框,我們會發現一般為了保證使用者體驗,彈框都會方便使用者進行一鍵消除,例如上述中雪球的各種彈框,可能點選一個叉號,可能任意點選其他地方,或者評價框這種直接點選“下次再說”等。

  • 彈框的處理效果

  • 自動化執行的任何時候,任意的彈框都可能出現,在這個時候用例不能失敗,需要將對應的彈框正確處理後繼續執行原用例,原用例的執行過程不受影響。

  1. 將需要處理的彈框元素加入到一個黑名單List中,遍歷List,透過findElements方法得到的List大小來判斷彈框元素是否存在,存在即點選處理
public static void handleAlert(){
        List<By> alertBox = new ArrayList<>();
        alertBox.add(By.id("ib_close"));   //廣告彈框
        alertBox.add(By.id("md_buttonDefaultNegative")); //評價彈框

        alertBox.forEach(alert->{
            By adsLocator = alert;
            List<WebElement> ads = driver.findElements(adsLocator);
            if (ads.size() >= 1) {
                ads.get(0).click();
            }
        });
    }

  1. 將handleAlert()方法加到driver.findElement方法之前,使定位前先判斷彈框的存在與否並進行處理
public static WebElement findElement(By by) {
            System.out.println(by);
            handleAlert();
            return driver.findElement(by);
            }

上述方法初步解決了彈框問題,但是缺點也很明顯。

缺點:每次定位元素前都需要處理彈框,影響執行效率,速度較慢 因此我們引入try catch來解決此問題

我們利用try catch的異常捕獲處理的機制,讓元素僅在定位失敗時才進入彈框處理handleAlert()方法,處理完畢後重新返回driver.findElement(by),對原case元素繼續進行定位執行;這樣就大大提升了處理效率,使處理更為精準。

public static WebElement findElement(By by) {
        try {
            System.out.println(by);
            return driver.findElement(by);
        } catch (Exception e) {
            System.out.println("進入彈框處理");
            handleAlert();
                return driver.findElement(by); 
            }
    }

遞迴處理:
一般情況下我們一次只會出現一個彈框,但是例外的是可能有一個以上的彈框同時出現,這樣的話雖然處理了其中一個彈框,但是剩下的彈框依然會阻斷用例的正常執行,這個時候就可以使用遞迴的方法,在處理完彈框後返回findElement方法自身,繼續進行try catch,使之進入彈框處理邏輯

public static WebElement findElement(By by) {
          try {
              System.out.println(by);
              return driver.findElement(by);
          } catch (Exception e) {
              System.out.println("進入彈框處理");
              handleAlert();
                  return findElement(by); 
              }
      }

注意:
使用遞迴方法後有一個問題,就是假如並不是因為某個彈框的出現而導致的定位失敗,而這個時候透過try catch進入到彈框處理邏輯後,由於並未匹配到彈框元素,所以遞迴就會進入一個死迴圈,不斷重複著彈框處理的邏輯,所以使用遞迴時我們也需要對其次數進行限制;一般兩個彈框同時出現已經算多的了,所以建議可以將遞迴的次數限制到最多兩次便退出。

static int i = 1;
public static WebElement findElement(By by) {
    try {
        System.out.println(by);
        return driver.findElement(by);
    } catch (Exception e) {
        if (i > 2){   //設定最多遞迴兩次
            i = 1;
            return driver.findElement(by);
        }
        System.out.println("進入彈框處理第"+i+"次");
        handleAlert();
        i++;
        return findElement(by); //最後呼叫自身完成遞迴,防止多彈框同時出現造成定位失敗
        }
}

按照上面的方法,看似已經很好的解決了彈框的處理,但是可以注意到的是:

  • 在檢查彈框的時候依然使用的是appium的定位,在當前頁面中根據元素的屬性去一一查詢定位
  • 所有的黑名單中的彈框都會被定位查詢一遍

而我們實際中最想要的也是最有效率的方法應該是:

  • 只有在當前頁面中存在的彈框才對其進行定位、操作、處理。為了達到我們想要的效果,就需要藉助於PageSource了。

1)appium的driver提供了一個getPageSource方法,此方法可以在當前頁面可以得到一個文字字串,也可以理解為當前頁面的xml,我們利用這種xml文字來進行判斷,就比用appium一一定位的方式要快速和精準的多了

String pageSource = driver.getPageSource();

2)設定黑名單,黑名單要使用元素的xpath,用來和PageSource文字做匹配,判斷此彈框是否存在當前頁面

String adBox = "com.xueqiu.android:id/ib_close";
String gesturePromptBox = "com.xueqiu.android:id/snb_tip_text";
String evaluateBox = "com.xueqiu.android:id/md_buttonDefaultNegative";

HashMap<String,By> map = new HashMap<>();
map.put(adBox,By.id("ib_close"));
map.put(gesturePromptBox,By.id("snb_tip_text"));
map.put(evaluateBox,By.id("md_buttonDefaultNegative"));

4)遍歷map,判斷黑名單彈框元素是否存在於當前pageSource,存在即根據彈框處理方式進行點選或其他操作(如上述中的新功能提示彈框,點選彈框自身無法消除,需點選頁面其餘部分方可消除)處理

map.entrySet().forEach(entry ->{
    if (pageSource.contains(entry.getKey())){
        if (entry.getKey().equals("com.xueqiu.android:id/snb_tip_text")){
            System.out.println("gesturePromptBox found");
            Dimension size = driver.manage().window().getSize();
            //點選螢幕的中心位置,消除新功能提示彈框
            new TouchAction<>(driver).tap(PointOption.point(size.width/2,size.height/2)).perform();
        }else {
          //其餘彈框直接點選消除
            driver.findElement(entry.getValue()).click();
        }
    }
});

//很多彈框的話,最好的是直接定位到到底哪個彈框在介面上,元素的判斷使用xpath
    public static void handleAlertByPageSource(){
        String pageSource = driver.getPageSource();//可以得到一個文字字串,也可以理解為當前頁面的xml
        //黑名單
        String adBox = "com.xueqiu.android:id/ib_close";
        String gesturePromptBox = "com.xueqiu.android:id/snb_tip_text";
        String evaluateBox = "com.xueqiu.android:id/md_buttonDefaultNegative";

        //將標記和定位符存入map
        HashMap<String,By> map = new HashMap<>();
        map.put(adBox,By.id("ib_close"));
        map.put(gesturePromptBox,By.id("snb_tip_text"));
        map.put(evaluateBox,By.id("md_buttonDefaultNegative"));

        //臨時修改隱式等待時間,防止查詢黑名單中元素過久
        driver.manage().timeouts().implicitlyWait(3, TimeUnit.SECONDS);

        //遍歷map,判斷黑名單彈框元素是否存在於當前pageSource,存在即點選處理
        map.entrySet().forEach(entry ->{
            if (pageSource.contains(entry.getKey())){
                if (entry.getKey().equals("com.xueqiu.android:id/snb_tip_text")){
                    System.out.println("gesturePromptBox found");
                    Dimension size = driver.manage().window().getSize();
                    new TouchAction<>(driver).tap(PointOption.point(size.width/2,size.height/2)).perform();
                }else {
                    driver.findElement(entry.getValue()).click();
                }
            }
        });
        //判斷完成後將隱式等待時間恢復
        driver.manage().timeouts().implicitlyWait(8,TimeUnit.SECONDS);
    }

6)最後將findElement方法中的handleAlert方法替換為handleAlertByPageSource方法即可

static int i = 1;
public static WebElement findElement(By by) {
    try {
        System.out.println(by);
        return driver.findElement(by);
    } catch (Exception e) {
        if (i > 2){   //設定最多遞迴兩次
            i = 1;
            return driver.findElement(by);
        }
        System.out.println("進入彈框處理第"+i+"次");
                handleAlertByPageSource();
        i++;
        return findElement(by); //最後呼叫自身完成遞迴,防止多彈框同時出現造成定位失敗
        }
}

再來解決首頁載入時可能出現的坑。

App 啟動載入時間較久(可能 App 本身載入慢,也可能是移動裝置本身載入應用速度慢,也可能首頁廣告時間較長),導致定位超時,用例失敗。對此我們又如下兩步解決辦法。

如標題所述,對首頁進入使用顯示等待,利用搜尋控制元件的出現來判斷是否進入了首頁,這樣不影響其他元素隱式等待的時間,也解決了首頁初始化載入時間過長的問題。

例如雪球僅在進入首頁後會出現 id為user_profile_container的使用者資訊控制元件,那麼我們就可以以此為依據來判斷應用是否載入完成進入了首頁。

在啟動方法中加入顯示等待上述首頁控制元件 30 秒,到控制元件可被定位時確認進入首頁。

new WebDriverWait(driver,30)
                .until(ExpectedConditions.visibilityOfElementLocated(By.id("user_profile_container")));

缺點:
但是這樣有個情況不能解決:若載入完成後有彈框出現,可能就一直無法定位到首頁元素,但是實際上已經載入完成,比如下圖的首頁廣告彈框 。

文章第二部分介紹了利用 PageSource 來判斷彈框是否存在的方法,在這裡依然適用,還是熟悉的味道,還是同樣的套路,將彈框元素 xpath 也加入 PageSource 判斷,這樣無論首頁控制元件和首頁彈框哪一個被發現,就都可以判斷應用已經載入完成,成功進入首頁,剩下的就可以交給用例和其他處理邏輯了

new WebDriverWait(driver,30)
                .until(x ->{
                    String xml = driver.getPageSource();
                    Boolean checkResult = xml.contains("user_profile_container") || xml.contains("com.xueqiu.android:id/ib_close");
                    System.out.println("主頁元素查詢的結果是:" + checkResult);
                    return checkResult;
                });

好了,經過上面的分析之後,我們終於搞定了入門 APP 自動化測試時的老大難問題。搞定了彈框及首頁啟動時載入完成如何判斷處理。

相關文章