腾讯Android自动化测试实战3.2.1 Robotium支持Native原理

3.2.1 Robotium支持Native原理

1. 获取控件原理

我们知道Android会为res目录下的所有资源分配ID,例如在布局xml文件中使用了 android:id="@+id/example_id",那么在Android工程编译时就会在R.java中相应地为该布局控件分配一个int型的ID,在Android工程中就可以通过Activity、Context或View等对象调用findViewById(int
id)方法引用相应布局中的控件。因此,在测试工程中,如果是在源码的情况下,测试工程可以引用被测工程的代码,也即可以直接获得被测工程中R.java中的ID,因此可以通过这种方式直接根据ID获取控件。Robotium中根据ID获取控件的实现即包含该方式,如代码清单3-5所示。

代码清单3-5 Getter.getView

public View
getView(int id, int index, int timeout){

    final Activity activity =
activityUtils.getCurrentActivity(false);

    View viewToReturn = null;

    //如果index小于1,则直接通过Activity的findViewById查找

    if(index < 1){

        index = 0;

        viewToReturn =
activity.findViewById(id);

    }

 

    if (viewToReturn != null) {

        return viewToReturn;

    }

 

    return waiter.waitForView(id, index,
timeout);

}

在getView(int id, int index, int timeout)方法中,先获取当前所在的Activity,然后直接通过findViewById(id)方法尝试获取控件,如果该方法能够正确获取,则直接返回;否则,使用waitForView(id,
index, timeout)方法进一步等待控件的出现。

对于测试工程没有关联被测工程的情况,是无法直接通过R.id.example_id的形式获取控件的,此时一般调用getView(String id)方法,即通过String型ID获取。之所以可以通过String型ID获取控件,是因为Robotium中该方法使用了Resources.getIdentifier(String name, String defType, String
defPackage)方法动态地将String型ID转换成了int型ID,如代码清单3-6所示。

代码清单3-6 Getter.getView(String
id,int index)

public View
getView(String id, int index){

    View viewToReturn = null;

    Context targetContext =
instrumentation.getTargetContext();

    String packageName =
targetContext.getPackageName();

    //先将String类型的ID转换成int型的ID

    int viewId =
targetContext.getResources().getIdentifier(id, "id", packageName);

 

    if(viewId != 0){

        viewToReturn = getView(viewId, index,
TIMEOUT);

    }

    //如果还未找到,则传入的ID可能是Android系统中的ID

    if(viewToReturn == null){

        int androidViewId =
targetContext.getResources().getIdentifier(id, "id",
"android");

        if(androidViewId != 0){

            viewToReturn =
getView(androidViewId, index, TIMEOUT);

        }

    }

 

    if(viewToReturn != null){

        return viewToReturn;

    }

    return getView(viewId, index);

}

因此,为了简化操作,我们完全可以统一使用getView(String id)方法来获取控件。

以上为根据ID获取控件的一种方式,另一种方式则是通过WindowManager获取所有View后再进行各种过滤封装。如代码清单3-7所示,在ViewFetcher中通过getAllViews方法获取所有的View,其中分别处理DecorView与nonDecorView。

代码清单3-7 ViewFetcher.getAllViews

public
ArrayList<View> getAllViews(boolean onlySufficientlyVisible) {

    //获取所有的DocorViews

    final View[] views = getWindowDecorViews();

    final ArrayList<View> allViews = new
ArrayList<View>();

    final View[] nonDecorViews =
getNonDecorViews(views);

    View view = null;

 

    if(nonDecorViews != null){

        for(int i = 0; i < nonDecorViews.length;
i++){

            view = nonDecorViews[i];

            try {

                addChildren(allViews,
(ViewGroup)view, onlySufficientlyVisible);

            } catch (Exception ignored) {}

            if(view != null)
allViews.add(view);

        }

    }

 

    if (views != null && views.length
> 0) {

        view = getRecentDecorView(views);

        try {

            addChildren(allViews,
(ViewGroup)view, onlySufficientlyVisible);

        } catch (Exception ignored) {}

 

        if(view != null) allViews.add(view);

    }

 

    return allViews;

}

如代码清单3-8所示,在getWindowDecorViews方法中通过使用反射获取Window-Manager中的mViews对象来获取所有DecorView,其中也可以看到对于Android系统版本大于19的处理是不同的。

代码清单3-8 ViewFetcher.getWindowDecorViews

@SuppressWarnings("unchecked")

public View[]
getWindowDecorViews()

{

    Field viewsField;

    Field instanceField;

    try {

//通过反射获取WindowManagerGlobal或WindowManagerImpl中的mViews变量

        viewsField =
windowManager.getDeclaredField("mViews");

//通过反射获取WindowManagerGlobal或WindowManagerImpl中的WindowManager实例的变量

        instanceField =
windowManager.getDeclaredField(windowManagerString);

        viewsField.setAccessible(true);

        instanceField.setAccessible(true);

        Object instance =
instanceField.get(null);

        View[] result;

        if (android.os.Build.VERSION.SDK_INT
>= 19) {

            result = ((ArrayList<View>)
viewsField.get(instance)).toArray(new View[0]);

        } else {

            result = (View[])
viewsField.get(instance);

        }

        return result;

    } catch (Exception e) {

        e.printStackTrace();

    }

    return null;

}

再看代码清单3-9中的WindowManagerString变量的来源,如代码清单3-9所示,WindowManagerString也同样地需要根据Android系统版本的不同而分别处理。

代码清单3-9 ViewFetcher.setWindowManagerString

private void
setWindowManagerString(){

    //不同的系统版本,WindowManager的变量名不同

    if (android.os.Build.VERSION.SDK_INT >=
17) {

        windowManagerString =
"sDefaultWindowManager";

    } else if(android.os.Build.VERSION.SDK_INT
>= 13) {

        windowManagerString =
"sWindowManager";

    } else {

        windowManagerString =
"mWindowManager";

    }

}

至此我们知道了Robotium中获取所有Views是通过反射机制实现的,而源码中的变量很可能根据版本的不同而改变,因此通过反射则往往需根据系统版本的不同而分别处理。所以,使用Robotium时最好使用开源项目中的最新版本,因为当有新的Android系统版本发布时,很可能Robotium也需要与时俱进地完善获取控件方式。

2. 控件操作原理

Robotium获取控件后,调用clickOnView(View view)方法就可以完成点击操作,这个方法可以实现两大功能:

根据View获取了控件在屏幕中的坐标。

根据坐标发送了模拟的点击操作。

如代码清单3-10所示,由于View本身可以获取到在屏幕中的起始坐标与控件长宽,因此通过getLocationOnScreen获取起始坐标后,再加上1/2的长与宽,即可计算出控件的中心点在屏幕中的位置。

代码清单3-10 Clicker.getClickCoordinates

private float[]
getClickCoordinates(View view){

    sleeper.sleep(200);

    int[] xyLocation = new int[2];

    float[] xyToClick = new float[2];

    //获取view的坐标,xyLocation[0]为x坐标的值,xyLocation[1]为y坐标的值

    view.getLocationOnScreen(xyLocation);

 

    final int viewWidth = view.getWidth();

    final int viewHeight = view.getHeight();

    //xyLocation中的值为控件左上角的坐标,因此xyLocation[0]+宽长除2即为该控件在x轴的中心点,同样地计算在y轴的中心点

    final float x = xyLocation[0] + (viewWidth
/ 2.0f);

    float y = xyLocation[1] + (viewHeight /
2.0f);

 

    xyToClick[0] = x;

    xyToClick[1] = y;

    return xyToClick;

}

知道了需要点击的位置后,那么接下来发送模拟点击就可以了。Android中的模拟操作可以通过MotionEvent来实现,而MotionEvent主要有以下三种形式:

MotionEvent.ACTION_DOWN:模拟对屏幕发送下按事件。

MotionEvent.ACTION_UP:模拟对屏幕发送上抬事件。

MotionEvent.ACTION_MOVE:模拟对屏幕发送移动事件。

Robotium中的点击屏幕方法即是通过MotionEvent实现的,如代码清单3-11所示,通过MotionEvent.obtain(long downTime, long eventTime, int action, float
x, float y, int metaState)方法获取相应的event事件后,再通过Instrumentation的sendPointerSync(MotionEvent event)方法将event事件实际地在手机上模拟执行。

代码清单3-11 Clicker.clickOnScreen

public void clickOnScreen(float
x, float y, View view) {

    boolean successfull = false;

    int retry = 0;

    SecurityException ex = null;

 

    while(!successfull && retry <
20) {

        long downTime =
SystemClock.uptimeMillis();

        long eventTime = SystemClock.uptimeMillis();

        MotionEvent event =
MotionEvent.obtain(downTime, eventTime,

            MotionEvent.ACTION_DOWN, x, y, 0);

        MotionEvent event2 =
MotionEvent.obtain(downTime, eventTime,

            MotionEvent.ACTION_UP, x, y, 0);

        try{

            //通过Instrumentation模拟发送下按操作

            inst.sendPointerSync(event);

            //通过Instrumentation模拟发送上抬操作,与下按操作结合,模拟完成了一个点击过程

            inst.sendPointerSync(event2);

            successfull = true;

        }catch(SecurityException e){

            ex = e;

            dialogUtils.hideSoftKeyboard(null,
false, true);

            sleeper.sleep(MINI_WAIT);

            retry++;

            View identicalView =
viewFetcher.getIdenticalView(view);

            if(identicalView != null){

                float[] xyToClick =
getClickCoordinates(identicalView);

                x = xyToClick[0];

                y = xyToClick[1];

            }

        }

    }

//如果点击失败,将抛出异常

    if(!successfull) {

        Assert.fail("Click at
("+x+", "+y+") can not be completed! ("+(ex != null ?
ex.getClass().getName()+": "+ex.getMessage() :
"null")+")");

    }

}

结合getClickCoordinates(View view)与clickOnScreen(float x, float y, View view)方法就完成了clickOnView(View view)方法的核心实现。通过控制不同手势操作的时间顺序还可以模拟各种手势操作,例如先发送MotionEvent.ACTION_DOWN,一段时间后,再发送MotionEvent.ACTION_UP就模拟了长按操作。先发送MotionEvent.ACTION_DOWN,然后发送MotionEvent.ACTION_MOVE,最后发送MotionEvent.ACTION_UP就是滑动操作了。因此,结合MotionEvent的各种模拟事件也可以自行实现自定义的手势操作。

3.2.2 Robotium支持WebView原理

在上一节中我们介绍了在Robotium中如何通过By.id或By.className方式获取Web-Element,那么Robotium中是如何获取到相应的HTML元素,并能知道元素坐标,从而发送点击事件的呢?

1. WebElement对象

Robotium中以WebElement对象对HTML元素进行了封装,在这个WebElement对象中包含locationX、locationY、ID、text、name、className、tagName等信息。

locationX、locationY:标识该HTML元素在屏幕中所在的X坐标和Y坐标。

ID、className:该HTML元素的属性。

tagName:该HTML元素的标签。

Robotium中封装了WebElement,提供了clickOnWebElement(WebElement webElement),ArrayList<WebElement>
getCurrentWebElements()等操作Web元素的API,对于在Android客户端中展示的Web页面,Robotium是如何把里面的元素都提取出来,并封装进WebElement对象中的呢?

如图3-13所示,通过getWebElements方法的调用关系图可以看出,Robotium主要通过JS注入的方式获取Web页面所有的元素,再对这些元素进行提取并封装成WebElement对象。在Android端与JS交互则离不开WebView和WebCromeClient。

 

 

图3-13 getWebElements方法的调用关系图

2. WebElement元素获取

1)利用JS获取页面中的所有元素

在PC上,获取网页的元素可以通过注入javascript元素来完成,以Chrome浏览器为例,打开工具—JavaScript控制台(快捷方式:Ctrl+Shift+J键),输入javascript:prompt (document.URL)即会弹出含当前页面的URL的提示框,因此通过编写适当的JS脚本就可以在这个弹出框中显示所有的页面元素。RobotiumWeb.js就提供了获取所有HTML元素的JS脚本。以Solo中getWebElements()为例,如代码清单3-12所示,可分为两步,先通过executeJavaScriptFunction()方法执行JS脚本,然后根据执行结果通过getWebElements返回。

代码清单3-12 WebUtils.getWebElements

public
ArrayList<WebElement> getWebElements(boolean onlySufficientlyVisible){

    boolean javaScriptWasExecuted =
executeJavaScriptFunction("allWebElements();");

 

    return
getWebElements(javaScriptWasExecuted, onlySufficientlyVisible);

}

如代码清单3-13所示,在executeJavaScriptFunction(final
String function)方法中通过webView.loadUrl(String
url)方法执行JS,而这里的WebView是通过getCurrentViews (Class<T> classToFilterBy, boolean
includeSubclasses)过滤出来的,且是过滤的android.webkit.WebView,这也是Robotium只支持系统WebView而不支持第三方浏览内核中的WebView的原因:

代码清单3-13 WebUtils.executeJavaScriptFunction

private boolean
executeJavaScriptFunction(final String function) {

    List<WebView> webViews =
viewFetcher.getCurrentViews(WebView.class, true);

    //获取当前屏幕中最新的WebView,即目标要执行JS的WebView

    //注:这里获取的WebView可能不是目标WebView,那么将导致获取WebElement失败

    final WebView webView =
viewFetcher.getFreshestView((ArrayList<WebView>) webViews);

   

    if(webView == null) {

        return false;

    }

    //执行JS前的准备工作,如设置WebSettings、获取JS方法等

    final String javaScript =
setWebFrame(prepareForStartOfJavascriptExecution(webViews));

       

    inst.runOnMainSync(new Runnable() {

        public void run() {

            if(webView != null){

    //调用loadUrl执行JS

                webView.loadUrl("javascript:"
+ javaScript + function);

            }

        }

    });

    return true;

}

想返回什么样的结果,关键在于执行了什么样的JS方法,Robotium中的getWeb-Elements()执行的JS方法是allWebElements(),代码片段可以通过RobotiumWeb.js找到,如代码清单3-14所示,采用遍历DOM的形式获取所有的元素信息。

代码清单3-14 RobotiumWeb.js中的allWebElements()

function
allWebElements() {

    for (var key in document.all){

        try{

           
promptElement(document.all[key]);          

        }catch(ignored){}

    }

    finished();

}

如代码清单3-15所示,将代码清单3-15中遍历获取到的每一个元素分别获取ID、text、className等,然后将元素通过prompt方法以提示框形式显示。在prompt时,会在ID、text、className等字段之间加上';,'特殊字符,以便解析时区分这几个字段。

代码清单3-15 RobotiumWeb.js中的promptElement(element)

function
promptElement(element) {

    var id = element.id;

    var text = element.innerText;

    if(text.trim().length == 0){

        text = element.value;

    }

    var name = element.getAttribute('name');

    var className = element.className;

    var tagName = element.tagName;

    var attributes = "";

    var htmlAttributes = element.attributes;

    for (var i = 0, htmlAttribute;
htmlAttribute = htmlAttributes[i]; i++){

        attributes += htmlAttribute.name +
"::" + htmlAttribute.value;

        if (i + 1 < htmlAttributes.length) {

            attributes += "#$";

        }

    }

 

    var rect = element.getBoundingClientRect();

    if(rect.width > 0 && rect.height
> 0 && rect.left >= 0 && rect.top >= 0){

        prompt(id + ';,' + text + ';,' + name +
";," + className + ";," + tagName + ";," +
rect.left + ';,' + rect.top + ';,' + rect.width + ';,' + rect.height + ';,' +
attributes);

    }

}

最后,执行finished()方法,调用prompt提示框,提示语为特定的'robotium-finished',用于在Robotium执行JS时,判断是否执行完毕,如代码清单3-16所示。

代码清单3-16 RobotiumWeb.js中的finished()

function
finished(){

    //robotium-finished用来标识Web元素遍历结束

    prompt('robotium-finished');

}

通过JS完成了Web页面所有元素的提取,提取的所有元素是以prompt方式显示在提示框中的,那么提示框中包含的内容在Android中怎么获取呢?

2)通过onJsPrompt回调获取prompt提示框中的信息

如代码清单3-17所示,通过JS注入获取到Web页面所有的元素后,可以通过onJsPrompt回调来对这些元素进行提取。Robotium写了个继承自WebChromeClient类的RobotiumWebClient类,覆写了onJsPrompt用于回调提取元素信息,如果提示框中包含“robotium-finished”字符串,即表示这段JS脚本执行完毕了,此时通知webElementCreator可以停止等待,否则,将不断将prompt框中的信息交由webElementCreator.createWeb-ElementAndAddInList解析处理。

代码清单3-17 RobotiumWebClient中的onJsPrompt

@Override

public boolean
onJsPrompt(WebView view, String url, String message, String defaultValue,
JsPromptResult r) {

    //当message包含robotium-finished时,表示JS执行结束

    if(message != null &&
(message.contains(";,") ||
message.contains("robotium-finished"))){

  

        if(message.equals("robotium-finished")){

    //setFinished为true后,WebElementCreator将停止等待

           
webElementCreator.setFinished(true);

        }

        else{

           
webElementCreator.createWebElementAndAddInList(message, view);

        }

        r.confirm();

        return true;

    }

    else {

        if(originalWebChromeClient != null) {

            return
originalWebChromeClient.onJsPrompt(view, url, message, defaultValue, r);

        }

        return true;

    }

 

}

3)将回调中获取的元素信息封装进WebElement对象中

获取到onJsPrompt回调中的元素信息后,接下来就可以对这些已经过处理、含特殊格式的消息进行解析了,依次得到WebElement的ID、text、name等字段。如代码清单3-18所示,将information通过特殊字符串“;,”分隔成数组对该字符串进行分段解析,将解析而得的ID、text、name及x,y坐标存储至WebElement对象中。

代码清单3-18 WebElementCreator中的createWebElementAndSetLocation

private WebElement
createWebElementAndSetLocation(String information, WebView webView){

    //将information通过特殊字符串“;,”分隔成数组

    String[] data =
information.split(";,");

    String[] elements = null;

    int x = 0;

    int y = 0;

    int width = 0;

    int height = 0;

    Hashtable<String, String> attributes
= new Hashtable<String, String>();

    try{

        x = Math.round(Float.valueOf(data[5]));

        y = Math.round(Float.valueOf(data[6]));

        width =
Math.round(Float.valueOf(data[7]));

        height = Math.round(Float.valueOf(data[8]));   

        elements =
data[9].split("\\#\\$");

    }catch(Exception ignored){}

 

    if(elements != null) {

        for (int index = 0; index <
elements.length; index++){

            String[] element =
elements[index].split("::");

            if (element.length > 1) {

                attributes.put(element[0],
element[1]);

            } else {

                attributes.put(element[0],
element[0]);

            }

        }

    }

    WebElement webElement = null;

    try{

    //设置WebElement中的各个字段

        webElement = new WebElement(data[0],
data[1], data[2], data[3], data[4], attributes);

        setLocation(webElement, webView, x, y,
width, height);

    }catch(Exception ignored) {}

 

    return webElement;

}

这样,把JS执行时提取到的所有元素信息解析出来,并储存至WebElement对象中,在获取到相应的WebElement对象后,就包括了元素的ID、text、className等属性及其在屏幕中的坐标,完成了对Web自动化的支持。

时间: 2024-08-25 05:06:26

腾讯Android自动化测试实战3.2.1 Robotium支持Native原理的相关文章

腾讯Android自动化测试实战3.2 Robotium原理简析

3.2 Robotium原理简析 如前文所述,一个基本的自动化测试用例主要分为获取控件.控件操作.断言三个步骤,而在实际编写测试用例的过程中,我们常常会遇到各种各样的问题,比如: 在这样的UI结构下该如何获取控件? 为何报这样或那样的错? 明明滑动了为何没有效果? 因为不同的项目有其自身的独特性与复杂性,没有任何书籍可以解决实际过程中遇到的所有问题,甚至即使求助Google搜索也可能得不到自己想要的答案.因此,对于任何一门技术而言都很有必要知其然并知其所以然,只有了解了其原理实现,才能更高效地运

腾讯Android自动化测试实战

腾讯Android自动化测试实战 丁如敏 盛娟 等著 图书在版编目(CIP)数据 腾讯Android自动化测试实战 / 丁如敏等著. -北京:机械工业出版社,2016.10 ISBN 978-7-111-54875-1 Ⅰ. 腾-   Ⅱ. 丁-   Ⅲ. 移动终端-应用程序–程序设计   Ⅳ. TN929.53 中国版本图书馆CIP数据核字(2016)第223713号 腾讯Android自动化测试实战 出版发行:机械工业出版社(北京市西城区百万庄大街22号 邮政编码:100037) 责任编辑:

腾讯Android自动化测试实战导读

前 言 Preface 为什么要写这本书 早在2010年年底,我们团队就有出一本关于移动互联网测试书籍的计划(那时候移动互联网测试书籍基本没有),当时计划的内容涉及面比较广,涵盖测试设计.测试用例管理.测试流程.自动化测试.专项测试等领域.不过,由于各种原因被搁浅,确实有点儿可惜,否则移动互联网测试国内的第一本书当时就面世了.这次终于又有机会整理这些年的测试经验并形成一本书了,借此可以跟业界的同行一起交流切磋. TMQ(Tencent Mobile Quality)腾讯移动品质中心,是腾讯内部最

腾讯Android自动化测试实战3.3.3 修改Robotium以支持X5WebView

3.3.3 修改Robotium以支持X5WebView 本节中的X5WebView指QQ浏览器团队出品的腾讯X5内核中的WebView.除了QQ.微信.应用宝等众多腾讯内部产品在使用X5内核外,京东.58同城等众多腾讯外部的合作伙伴也在使用X5内核. 腾讯X5网站:http://x5.tencent.com/. 然而Robotium本身并不支持获取X5WebView中的元素,因此无法对使用了X5内核的Web页面进行自动化测试,而通过3.2.2节中介绍的Robotium支持WebView原理可知

腾讯Android自动化测试实战1.1 Android自动化测试框架概述

1.1 Android自动化测试框架概述 2007年Android开源时,Monkey.Instrumentation和MonkeyRunner这3个测试框架,是跟Android源码一起发布的,这也是最早可用的自动化测试框架,那几年大家基本都是用这些框架来开展自动化相关测试工作的.2010年,第一个第三方的测试工具Robotium(基于Instrumentation)发布了,不少测试人员就转用这个框架,Robotium社区逐步发展起来.图1-1所示为Robotium热度随时间变化的趋势. 201

腾讯Android自动化测试实战第2章

第2章 自动化测试框架及应用领域综述 近几年,随着移动互联网的快速发展,智能终端的App应用越来越广,Android测试技术也备受重视,新的终端自动化测试框架层出不穷,本章笔者就自动化测试的入门知识及其应用做一个浅显的梳理与总结,与读者一同探讨移动终端自动化测试思路和方案.同时,本书主要也是围绕本章节提到的基础框架及其应用场景进行实战分析与演练,以亲身体验总结出实际项目经验,给准备实施或正在实施自动化测试的读者提供一些帮助和建议. 自动化测试在软件测试的各大沙龙.行业峰会以及培训课程中都是一个热

腾讯Android自动化测试实战第3章

第3章 Robotium框架工作原理及实践 2010年,当Android还处于发展早期时,拥有丰富自动化测试经验的Renas Reda创建了Robotium项目,在Robotium发展到4.0版本时开始支持App中的Web自动化,经过几年的发展,Robotium现在已经是一款成熟.全面.稳定的自动化测试框架.更重要的是,Robotium是一款开源的测试框架,在世界各地都有活跃的贡献者对其进行更新与维护,因此,无须担心将来Robotium会随着Android的发展而变得不可用.不易用,相反,Rob

腾讯Android自动化测试实战2.1.2 自动化测试框架基本原理

2.1.2 自动化测试框架基本原理 经过前面的一个简单的自动化测试案例,我们对Android的自动化测试有了一个感性的认识,很多有相关工作经验的测试同学也都会理解,这和PC的自动化测试思路是相通的,只不过所借助的框架不同,目前业界已经有很多成熟的开源Android端自动化测试框架,经常用到的框架代表有Robotium和UI Automator,各个框架可能在具体应用上有些不同,如有些偏稳定性,有些适用于Web应用,有些能支持跨应用,等等,但其主要思想是通过控件的位置.名称.属性等获取控件对象,并

腾讯Android自动化测试实战3.4 本章小结

3.4 本章小结 本章分三小节,从功能.原理及实践三方面介绍了Robotium测试框架,第一小节先全面概览似的介绍了Robotium的整体,然后从控件获取.控件操作.WebView支持.断言等维度介绍了相应功能及其使用方法,力图让读者知道如何使用Robotium测试框架来进行用例编写.第二小节则分别从Native和Web角度介绍了Robotium的实现原理,力图让读者了解更多的为什么,从而可以在实际项目中更灵活地使用Robotium编写测试用例.第三小节则从实践运用角度选取一般项目中常见的一些场