iOS 测试 Appium IOS 自动化测试速度优化 -- 稳定高效的方法 (Appium 1.53 +IOS 9.1)

土豆 · 2017年06月22日 · 最后由 阳光 回复于 2021年01月21日 · 4587 次阅读

用 Appium 做过 IOS 自动化的都知道,ios 下的自动化是特别慢的,有时候查找一个控件居然能耗时两三分钟,试问这样的速度谁能忍受的了,有些公司为此干脆就做 IOS 自动化了。难道 IOS 的自动化就真的是这么慢么?非也,慢是因为 appium 的某些方法没有实现好,经过优化之后是可以摆脱这个问题的,下面给大家讲解下 appium ios 的优化策略。

关于 appium ios 速度策略优化,网上能够搜索到的好像就是只有一篇,https://testerhome.com/topics/3352,百度很多网站也是转发的这一篇文章。这篇文章说了大概的原因却没有根本的解决问题。

我们知道影响 ios 的自动化速度慢的原因主要就是 2 个方面:

1、输入操作 ,这个跟 android 是一样的情况

2、XPATH 查找 特别耗时。
跟页面控件的多少有关,控件多的页面比如首页,就出现查找元素耗费两三分钟的情况。这主要是因为 appium 的 xpath 实现方式不好造成的,appium 实现 xpath 查找是先通过苹果的 UIAutomation 框架 instrument JS UIATarget.localTarget().logElementTree();获取到控件树结构,再把它格式化成 XML,然后再通过 xpath 遍历查找到控件,最后又转换为底层的 instrument JS 去执行操作。这其中的获取跟解析都特别耗时。

针对这两个问题,之前文章提到的方法是,尽量少用 xpath 定位方式,尽量减少通信,尝试使用通过 driver.getPageSource() 后自己重新查找方法。输入操作用 setvalue 代替。但是你会发现 ios 下的 app 根本现在根本没有 id 属性,更可恶的是有大部分的控件 name,label 等属性都是空的(虽然界面有文字)只能通过又丑又长的 xpath 表达式定位,例如:
//UIAApplication[1]/UIAWindow[1]/UIATableView[1]/UIATableCell[1]/UIAButton[2]。这种 xpath 查找效率是很低的。所以虽然 xpath 可恨但我们还是不可避免。对于重新查找方法更不可取,一是对编程要求高,二是不知道有多少坑。对于减少通信的做法,只能适应于本地模式,如果是这种方式,不如直接用 UIAutomation,用啥 appium。用 appium 就是为了解决不同设备端的整合,分布式并发执行问题的,而这免不了通信,所以前面的文章并没有把问题根本解决。

下面给大家讲解下我的解决思路:

通过分析,我发现 appium 解析耗费大量时间解析 xpath 最终又是转换为 instrument JS,为什么我们不能绕过解析 xpath 过程,直接把 xpath 翻译成 instrument JS?答案是肯定的,庆幸 appium 提供了直接执行 instrument JS 的定位方式 ByIosUiautomaiton.

经过优化后,ios 自动化的执行速度得到了大幅提升,原来操作一个控件需要两三分钟的,也只需要 2 秒左右了!

具体代码实现如下:(有兴趣的可以自己去查看下 instrument JS 语法)

1、xpath 定位替换代码


/**
 * //UIAApplication[1]/UIAWindow[1]/UIATableView[1]/UIATableCell[1]/UIAButton[3]
 * @param str
 * @return
 */
private  String xpathToIosLocatorJs(String str){
    String[] array=str.trim().replace("//","/").split("/");
    for (int i=1;i<array.length;i++){
        String tag=array[i];
        String[] tagArray=tag.split("\\[");
        String tagName=tagArray[0];
        if (tagArray.length==2){
            if (!tagName.contains("UIAApplication")&&!tagName.contains("UIAWindow")){
                String indexStr=tagArray[1].replace("]","");
                if (checkStringIsInt(indexStr)){
                    int  index=Integer.valueOf(indexStr)-1;
                    indexStr=String.valueOf(index);
                    tag=tag.replace("["+tagArray[1],"["+indexStr+"]");
                }else {
                    indexStr="\""+indexStr.split("=")[1].replace("\"","").replace("'","")+"\"";
                    tag=tag.replace("["+tagArray[1],"["+indexStr+"]");
                }
            }
        }
        switch (tagName){
            case "UIAApplication":
                tagName="frontMostApp";
                break;
            case "UIAWindow":
                tagName="mainWindow";
                break;
            case "UIATableCell":
                tagName="cells";
                break;
            case "*":
                tagName="frontMostApp().mainWindow().elements";
                break;
            default:
                tagName=tagName.replace("UIA","")+"s";
                break;
        }
        tagName=tagName+"()";
        StringBuffer sb=new StringBuffer();
        if (!tagName.equals("")){
            sb.append(tagName.charAt(0));
            tagName=tagName.replace(tagName.charAt(0),sb.toString().toLowerCase().charAt(0));
        }
        tag=tag.replace(tagArray[0],tagName);
        if (tagName.contains("frontMostApp")||tagName.contains("mainWindow")){
            tag=tag.replace("["+tagArray[1],"");
        }
        str=str.replace(array[i],tag);
    }
    str=" var webElement="+str.replace("//","UIATarget.localTarget().").replace("/",".")+";";
    return str;

}


private String xpathToUiautomatorJs(String str){
    str=xpathToIosLocatorJs(str);
    str=str.replace("//","target.").replace("/",".").replace("var webElement=","");
    String[] array=str.split("\\.");
    for (int i=0;i<array.length;i++){
        String tagName=array[i];
        if (tagName.contains("UIATarget")||tagName.contains("localTarget")||tagName.contains("frontMostApp")||tagName.contains("mainWindow")){
            str=str.replace(array[i],"");
        }else {
            break;
        }
    }
    str=str.substring(3,str.length()).replace(";","");
    return str;
}
//获取元素
public WebElement getWebelement(Locator locator,WebDriver driver)
{
    WebElement webElement;
    switch (locator.getLocatorType())
    {
        case "xpath" :
            if (driver instanceof IOSDriver){
                IOSDriver iosDriver=(IOSDriver) driver;
                String uiautomatorJs=xpathToUiautomatorJs(locator.getLocatorValue());
                log.info("将xpath转换成uiautomatorJs"+uiautomatorJs);
                webElement=iosDriver.findElementByIosUIAutomation(uiautomatorJs);
            }else if (driver instanceof AndroidDriver){
                String locatorValue=locator.getLocatorValue();
                if (locatorValue.contains("//*[@text")){
                    String text=locatorValue.split("=")[1].replace("'","").replace("]","").replace("\"","");
                    String uiautomatorExpress="new UiSelector().text(\""+text+"\")";
                    webElement=((AndroidDriver) driver).findElementByAndroidUIAutomator(uiautomatorExpress);
                }else if (locatorValue.contains("//*[contains(@text")){
                    String text=locatorValue.split(",")[1].replace("'","").replace("]","").replace("\"","").replace(")","");
                    String uiautomatorExpress="new UiSelector().textContains(\""+text+"\")";
                    webElement=((AndroidDriver) driver).findElementByAndroidUIAutomator(uiautomatorExpress);
                }else {
                    webElement=driver.findElement(By.xpath(locator.getLocatorValue()));
                }
            }else {
                webElement=driver.findElement(By.xpath(locator.getLocatorValue()));
            }
            break;
        case "id":
            webElement=driver.findElement(By.id(locator.getLocatorValue()));
            break;
        case "cssSelector":
            webElement=driver.findElement(By.cssSelector(locator.getLocatorValue()));
            break;
        case "name":
            webElement=driver.findElement(By.name(locator.getLocatorValue()));
            break;
        case "className":
            webElement=driver.findElement(By.className(locator.getLocatorValue()));
            break;
        case "linkText":
            webElement=driver.findElement(By.linkText(locator.getLocatorValue()));
            break;
        case "partialLinkText":
            webElement=driver.findElement(By.partialLinkText(locator.getLocatorValue()));
            break;
        case "tagName":
            webElement=driver.findElement(By.tagName(locator.getLocatorValue()));
            break;
        case "iosUIAutomation":
            if (driver instanceof IOSDriver){
                webElement=((IOSDriver) driver).findElementByIosUIAutomation(locator.getLocatorValue());
            }else {
                webElement=driver.findElement(By.xpath(locator.getLocatorValue()));
            }
            break;
        default :
            webElement=driver.findElement(By.xpath(locator.getLocatorValue()));
            break;

    }
    return webElement;
}

2、input 输入替代方法代码

/**
 * 文本框输入操作
 * @param locator  元素locator
 * @param value 输入值
 */
public void type(Locator locator,String value,WebDriver driver)
{
    try {
        WebElement webElement=findElement(locator,driver);
        if (driver instanceof AppiumDriver){
            if (driver instanceof  AndroidDriver){
                AppiumDriver androidDriver=(AppiumDriver) driver;
                if (androidDriver.getContext().contains("NATIVE_APP")){
                    MobileElement mobileElement= (MobileElement)webElement;
                    mobileElement.setValue(value);
                }else {
                    webElement.sendKeys(value);
                }
            }else {
                MobileElement mobileElement= (MobileElement)webElement;
                mobileElement.setValue(value);
            }
        }else {
            webElement.sendKeys(value);
        }
        log.info("input输入:"+locator.getLocatorName()+"["+"By."+locator.getLocatorType()+":"+locator.getLocatorValue()+"value:"+value+"]");
    } catch (Exception e) {
        log.error("找不到元素,input输入失败:"+locator.getLocatorName()+"["+"By."+locator.getLocatorType()+":"+locator.getLocatorValue()+"]");
        e.addSuppressed(new Exception(""));
        e.addSuppressed(new Exception("找不到元素,input输入失败:"+getLocatorInfo(locator)));
        e.printStackTrace();
        throw e;
    }

}

附:locator 对象


/**
 * Created by zhengshuheng on 2017/2/16 0016.
 */
public class Locator  implements Serializable{
    @ApiModelProperty(value = "元素Id")
    private Integer locatorId;
    @ApiModelProperty(value = "元素编码")
    private String  locatorNum;
    @ApiModelProperty(value = "元素名称")
    private String  locatorName;
    @ApiModelProperty(value = "定位方式")
    private String  locatorType;
    @ApiModelProperty(value = "定位内容")
    private String  locatorValue;
    @ApiModelProperty(value = "页面ID")
    private Integer pageId;
    //是否唯一
    private Integer isOnlyOne;

    public Integer getLocatorId() {
        return locatorId;
    }

    public void setLocatorId(Integer locatorId) {
        this.locatorId = locatorId;
    }

    public String getLocatorNum() {
        return locatorNum;
    }

    public void setLocatorNum(String locatorNum) {
        this.locatorNum = locatorNum;
    }

    public String getLocatorName() {
        return locatorName;
    }

    public void setLocatorName(String locatorName) {
        this.locatorName = locatorName;
    }

    public String getLocatorType() {
        return locatorType;
    }

    public void setLocatorType(String locatorType) {
        this.locatorType = locatorType;
    }

    public String getLocatorValue() {
        return locatorValue;
    }

    public void setLocatorValue(String locatorValue) {
        this.locatorValue = locatorValue;
    }

    public Integer getPageId() {
        return pageId;
    }

    public void setPageId(Integer pageId) {
        this.pageId = pageId;
    }

    public Integer getIsOnlyOne() {
        return isOnlyOne

原创文章,转载请注明出处

原文:http://www.webdriver.org/article-58-1.html

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 20 条回复 时间 点赞

我最近就是在用 instrument JS 来写 IOS ui 自动化,说说感受吧
1、确实快了不少,但是也没有非常非常快,和 Android 还是有差距,不过和直接用 ID、Xpath 也算欣慰了。
2、因为是跨平台的语言,所以没法获取到返回的元素对象,代码发过去以后就没以后了,也没办法 try catch,或者书写更加复杂的 instrument JS 来控制查找到元素。appium 提供的所有等待方法算是崩了。
3、我的处理方法是进入一个页面以后先用 appium 的方法确定所有元素就位,然后再使用 instrument JS 来操作元素。稳定性当然是没有 appium 的方法高。
4、提供一个小技巧,instrument JS 也有等待的方法,但是是类似休眠的定时等待(当然还有更高级的我不会搞)UIATarget.localTarget().delay(2);如果确定能够找到页面的各项元素,可以结合等待一次性发送大量语句,效率提高很多。(稳定性你懂的)

土豆 #20 · 2017年06月22日 Author
Karaser 回复

稳定性这一块我是用了 webdrvier 的 webdriverwait 机制,智能等待,不过即便这样 ios 还有一个坑,就是有时候很快找到了控件,但是文本还没有出现获取到的文本是空的,所以再这种情况下,又加了一次 webdriverwait。跟 Android 的差距(目前用的 uiautomator 模式,2.0 调通一次后,真机一直启动不了 uiautomator2 server 服务,而虚拟机却没问题,很是郁闷,也没具体对比出两者的速度)的话,感觉没什么差距用这种方法后感觉比它那个快一点,唯独就是不知道 appium 审查出来的的元素,为毛 ios 的大部分控件是没有属性值的。

19楼 已删除

还在用 instrument js 呀,不是都废弃了吗

codeskyblue 回复

那请问现在流行什么呢?

codeskyblue 回复

测试机现在还没有 9.3 以上的所以用不了 xcutest,所以我这边是做了,兼容处理,针对不同 ios 版本调用不同的框架

Karaser 回复

WebDriverAgent

codeskyblue 回复

9.2 的设备。。

赞一个,不过这套方法只能适用于 10 以下的设备了

土豆 #14 · 2017年06月23日 Author
fdeferf 回复

10 以上的底层是用 wda,不确定是否也有这种问题,但由于目前没机器,没有去验证是否也有这种问题,如果也存在这种很慢的情况,应该也是可以按照这种思路处理

土豆 回复

对,说错了,思路是适合的,xpath 的使用优先级降低

分析问题并优化解决,棒棒的

现在 ios 基本不用 xpath,我自己一般用 iosNsPredicate,完全可以替代 xpath,原生支持,速度更快

我是测试小白,appium 1.6.5 ,xcode8.2,在 Mac 机上运行,Mac 机的版本是 10.12.5
遇到问题是
还请大神们帮忙指教

浅浅 回复

缺少 platformName 字段

DC 回复

请问 iosNsPredicate 具体咋用呢?

Wang 回复

我有发帖出来的,你可以看看

如果 type 是唯一的,就用 class_name;如果有 name、id 最好了,使用过程中觉得 class_name 和 id、name 比 ios_predicate 快

Karaser 回复

您好 我想请加几个 ios 自动化的问题 您方便加我的微信吗 我怕不能及时看到您的回复 zhouy0802 谢谢

DC 回复

那请问如果用您说的这种方式 那里边填写什么内容啊 括号里

DC 回复

这个 ios 自动化速度实在崩溃 我目前是 xcode10.1 ios12.4.9 appium1.15

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册