腾讯Android自动化测试实战3.1.4 Robotium的控件获取、操作及断言-阿里云开发者社区

开发者社区> 华章出版社> 正文

腾讯Android自动化测试实战3.1.4 Robotium的控件获取、操作及断言

简介:

3.1.4 Robotium的控件获取、操作及断言

Robotium是一款在Android客户端中的自动化测试框架,它需要模拟用户操作手机屏幕。要完成对手机的模拟操作,应该包含以下几个基本操作:

(1)需要知道所要操作控件的坐标。

(2)对要操作的控件进行模拟操作。

(3)判断操作完成后的结果是否符合预期。

因此,本节将从控件获取、控件操作及操作后断言来介绍Robotium,此外,由于WebView在控件获取和控件操作上都与Native完全不同,将对其做单独介绍。

1. Native控件获取

从Robotium中获取Native控件主要有两大方式:一个是根据被测应用的控件ID来获取;另一个是先获取当前界面所有的控件,对这些控件进行过滤封装后再提供相应的获取控件的API。

1)根据被测应用的控件ID来获取

根据控件ID获取见表3-1。

根据String型ID获取控件:

ImageView mIcon = (ImageView) solo.getView("mypic");

在Android中,所有的控件都继承自View,因此,如果被测应用中的控件有唯一ID的话,就可以使用这种通过ID形式唯一获取所要操作的控件。

例如获取RelativeLayout或LinearLayout:

RelativeLayout rel = (RelativeLayout) solo.getView("example1");

LinearLayout lin = (LinearLayout) solo.getView("example2");

由于Android中所有的控件都继承自View类,而对于开发人员的自定义控件,这些自定义控件也基本是继承自Android的基础控件扩展而来的,因此通过这种方式几乎可以获得所有类型的控件,获取相应类型的控件时只要进行转义即可,因此当控件拥有唯一ID时,推荐使用该方式。

控件ID可以通过Android SDK中提供的工具来查看,例如%ANDROID_HOME%\tools\uiautomatorviewer.bat工具,在Android 4.3及以上系统版本的手机上,可直接查看到UI界面的ID。

2)根据控件类型的索引、文本来获取

根据文本获取见表3-2。

此方式是Robotium先将当前界面中的所有控件全部获取,然后按控件类型、索引进行过滤后再获取指定的控件View。

根据index索引获取控件:

//返回界面中第一个类型为Button的控件

Button loginBtn = (Button) solo.getButton(0);

其他的如getEditText(int index)、getText(int index)均同理。

根据文本text获取控件:

//返回界面中文本为‘登录’类型为Button的控件

Button loginBtn = (Button) solo.getButton("登录");

其他的如getText(String text)、getEditText(String text)均同理。

在Robotium中查找控件时,如果找不到相应ID或文本的控件,测试框架会throw出“View with id ××× is no found”或者“with text ××× no found”等Throwable异常,若我们并不希望因此而报错,则可以使用try catch Throwable来捕获。

3)根据控件类型进行过滤

根据类型过滤见表3-3。

表3-3 根据类型过滤

返回值  方法及说明

ArrayList<View>      getCurrentViews()

获取当前界面或弹框中所有的控件

ArrayList<T>     getCurrentViews(Class<T> classToFilterBy)

获取当前界面或弹框中所有控件类型为classToFilterBy的控件

ArrayList<T>     getCurrentViews(Class<T> classToFilterBy, View parent)

获取父控件parent下所有控件类型为classToFilterBy的控件

 

获取当前界面或弹框中所有控件类型为TextView的控件:

ArrayList<TextView> allTextViews = solo

              .getCurrentViews(TextView.class);

获取指定父控件下所有控件类型为TextView的控件:

RelativeLayout rel = (RelativeLayout) solo.getView("example1");

ArrayList<TextView> allTextViews = solo

              .getCurrentViews(TextView.class, rel);

同样是过滤出指定的控件类型,不过该方法是从父视图parent中开始过滤,当不指定parent,即solo.getCurrentViews(TextView.class,null)时,则和solo.getCurrentViews(TextView.class)一样,返回的是当前界面中所有的。

移动App一般节奏很快,UI布局结构也经常随着功能的变更而变动,例如“登录”按钮从最上面变到了最下面,因此通过索引或文本来获取控件是有很大隐患的。很多时候,通过巧妙地控件过滤可以更准确地找到相应的控件。

2. Native控件操作

对于Android端的自动化测试而言,当我们获取到期望的控件后,接下来就是对该控件进行点击、长按、文本输入、拖动等模拟操作。除此之外,UI自动化测试为了贴近用户的真实使用及自身健壮性,还需要时延等待、页面加载等待;为了判断界面是否符合预期,则还需要控件搜索、界面截图等操作。

1)点击、长按操作

点击长按见表3-4。

表3-4 点击长按

返回值  方法及说明

void      clickOnView(View view)/clickLongOnView(View view)

点击指定的View控件/长按指定的View控件

void      clickOnScreen(float x, float y)/clickLongOnScreen(float x, float y)

根据坐标x, y点击屏幕/根据坐标x, y长按屏幕

 

Robotium是基于控件的自动化测试框架,当获取到要操作的控件后,直接对控件进行点击、长按或文本输入等操作即可。

点击指定的View控件:

Button loginBtn = (Button) solo.getView("loginBtn");

solo.clickOnView(loginBtn)

Robotium还提供了点击文本、点击图片的API,例如clickOnText(String text)、click-OnButton(String text)等,这类API类似于前文所介绍的,先根据文本获取控件,再发送点击事件:

Button loginBtn = (Button) solo.getButton("登录");

solo.clickOnView(loginBtn)

类似于点击、长按指定的View控件:

Button loginBtn = (Button) solo.getButton("登录");

solo.clickLongOnView(loginBtn)

需要注意的是,Robotium的点击事件是通过Instrumentation发送的,因此该类点击方法不能点击非被测应用的区域,例如不能点击至通知栏所在的区域,否则会出现类似如下的异常:

java.lang.SecurityException: "Injecting to another application requires INJECT_EVENTS permission"

因此在使用Robotium编写测试用例时,需要注意其无法跨应用的缺点,从而尽量避免出现此场景,有些场景偶然性地无法规避,可以采用try catch Throwable的形式捕获异常,而对于需要跨应用的场景,则可以使用9.4.2节介绍的UI Automator结合Instrumentation模式进行处理。

try {

    } catch (Throwable e) {

}

在手机设置–开发者选项中,可以开启“指针位置”,开启“指针位置”后,再触摸屏幕时,可实时显示屏幕坐标。调试时为了更准确地知道对屏幕的什么地方进行了操作,也常常同时开启“显示触摸操作”开关。

2)操作输入框

操作输入框见表3-5。

表3-5 操作输入框

返回值  方法及说明

void      enterText(EditText editText, String text)

在指定的EditText中输入文本text

void      typeText(EditText editText, String text)

在指定的EditText中键入文本text

void      clearEditText(EditText editText)

清空指定的输入框

 

在自动化测试过程中,当我们可以准确获取控件,并能模拟点击、长按等基本操作后,就可以在被测应用中进行自由跳转,然后可能就需要进行一些输入操作。测试框架中主要提供了enterText(EditText editText, String text)和typeText(EditText editText, String text)两种方法,前者直接对EditText文本框进行赋值,不会有文本输入的展示过程,而后者则会一个文本一个文本地输入,更贴近真实用户的操作。

EditText userET = (EditText) solo.getView("example_et_id");

solo.enterText(userET, "my_user_name")      //直接对文本框赋值

solo.typeText(userET, "my_user_name")       //会展示输入的过程

3)滑动、滚动

滑动、滚动见表3-6。

表3-6 滑动、滚动

返回值  方法及说明

void      drag(float fromX, float toX, float fromY, float toY, int stepCount)

从起始x,y坐标滑至终点x,y坐标;通过stepCount参数指定滑动时的步长

void      scrollToTop() / scrollToBottom()

滚动至顶部 / 滚动至底部

void      scrollUp() / scrollDown()

向上滚动屏幕 / 向下滚动屏幕

void      scrollListToLine(AbsListView absListView, int line)

滚动列表至第line行

 

在Android中,常用的操作还有各种滑动手势,如上拉、下拉、左滑、右滑等。在滑动方面,测试框架主要提供了两类支持,一类是根据坐标进行滑动从而可以模拟各类手势操作,另一类则是根据控件来直接进行滚动操作。

根据坐标进行滑动的主要是drag(float fromX, float toX, float fromY, float toY, int step Count),这里的参数包括起始位置的x与y坐标、终点位置的x与y坐标,还有步长stepCount。其中步长stepCount的意思是,假如要从A点滑到B点,如果步长为1,那么将直接产生从A点到B点的手势操作,滑动速度很快;如果步长为100,则将从A到B分成100等份,例如A、A1、A2…B,然后依次从A滑到A1,再从A1滑到A2、A2滑到A3……这样滑动更慢但结果也更精确,例如当我们在手机上快速从下往上滑动时,列表滑动是有惯性的,会快速滚动,而这常常不是我们所需要的。

根据控件进行滚动主要有滚动至顶部、底部等方法。scrollToTop()方法可以将当前屏幕滑至顶部,如果当前是ListView则滑至列表的顶部,如果是WebView则滑至页面的顶部。同样地,scrollToBottom()可将界面滑至底部。类似的还有向下滑一屏的scrollDown()方法和向上滑一屏的scrollUp()方法。与前文介绍的drag方法不同的是,这类滚动调用的是相应控件自身的API,例如WebView的滚动调用的是控件自身的pageUp(boolean top)或pageDown(boolean bottom)方法。因此,这种方式与drag方式最大的区别在于,drag是实际地模拟手势操作,当上拉时,如果ListView有监听上拉加载更多,那么使用drag是可以触发上拉加载更多的,而scrollUp()则不能。

4)搜索与等待

搜索与等待见表3-7。

表3-7 搜索与等待

返回值  方法及说明

void      sleep(int time)

休眠指定的时间,单位毫秒

boolean searchText(String text)

从当前界面搜索指定文本

boolean waitForView(int id) / waitForText(String text)

等待指定控件出现 / 等待指定文本出现

boolean waitForActivity(String name)

等待指定的Activity出现

boolean waitForLogMessage(String logMessage)

等待指定的日志信息出现

boolean waitForDialogToOpen() / waitForDialogToClose()

等待弹框打开 / 等待弹框关闭

 

UI自动化测试常常被诟病运行不稳定,除了项目快速迭代导致界面经常变更这一不可控因素外,脚本常常运行出错就是由于没有合适的等待机制导致控件未找到、点击异常等问题,要想测试用例能够快速且稳定地运行,合理使用等待是关键要素之一。

Robotium中提供了诸多与等待相关的API,但是实际情况中需要等待的操作往往要复杂得多,因此测试框架中也提供了Condition模式,即waitForCondition(Condition condition, int timeout)方法,使用该方法时,实现Condition接口并重写isSatisfied()方法,isSatisfied()为true时将跳出等待。通过这种模式我们可以自定义实现更多类型的等待操作,如代码清单3-1所示。

代码清单3-1 使用waitForCondition模式实现等待

public void waitForAppInstalled(final String appName, int timeout) {

    waitForCondition(new Condition() {

        @Override

        public boolean isSatisfied() {

            sleeper.sleepMini();

            return checker.isAppInstalled(appName);

        }

    }, timeout);

}

当然了,我们也可以使用超时机制来实现,如代码清单3-2所示。

代码清单3-2 使用TimeOut模式实现等待

public void waitForAppInstalled(final String appName, int timeout) {

    long endTime = SystemClock.uptimeMillis() + timeout;

    while (SystemClock.uptimeMillis() < endTime) {

        if (checker.isAppInstalled(appName)) {

            break;

        }

        sleeper.sleep();

    }

}

需要注意的是,Robotium中查找控件、点击控件等API都默认使用了搜索与等待机制,当我们使用上文提到的获取控件、点击控件相关操作时,测试框架已经做好了等待操作,因此非特殊情况是不需要额外增加等待操作的步骤的。太多的等待将使用例执行变得缓慢低效,因此在用例编写调试过程中应该做好平衡。

5)截图及其他

截图及其他见表3-8所示。

表3-8 截图及其他

返回值  方法及说明

void      takeScreenshot(String name)

截图,图片名称为指定的name参数,图片默认路径为/sdcard/Robotium-Screenshots/

void      finishOpenedActivities()

关闭当前已打开的所有Activity

void      goBack() / goBackToActivity(String name)

点击返回键 / 不断地点击返回键直至返回到指定的Activity

void      hideSoftKeyboard()

收起键盘

void      setActivityOrientation(int orientation)

等待设置Activity转屏方向

 

自动化测试过程中,因为都是自动化执行的,当用例执行失败时,除了日志外,最方便解决定位问题的就是运行时的截图,有了截图定位问题往往事半功倍,Robotium中提供了单次截图及截取一系列图片的功能。takeScreenshot()方法可以直接截取当前屏幕,并将其默认地保存在/sdcard/Robotium-Screenshots/目录下,要更改图片名称则使用takeScreenshot(String name),要截取某时间段内一个序列的话则可以使用startScreenshotSequence(String name)。那么如何更好地在自动化中使用截图功能呢?一般情况下我们更希望的是在用例执行失败时进行截图,详情请见本书9.3.2节中介绍的结合Spoon出错重试与截图。

除了常规的操作外,Robotium测试框架还提供了发送模拟按键sendKey(int key)、设置屏幕是横屏还是竖屏setActivityOrientation(int orientation)、模拟点击返回键goBack()、跳转至指定Activity的方法goBackToActivity(String name)、收起输入法hideSoftKeyboard()、关闭所有已打开的Activity 的方法finishOpenedActivities()等。通过组合利用这些常用操作,基本就可以完成在Android端的UI自动化操作了。

3. WebView支持

在Android App中由于HTML可以更快地响应变化,而不像Native那样需要发布版本才能让用户使用上新特性,因此大多数App都是既有Native部分,也有HTML部分,也即俗称的Hybrid App。而Robotium在Robotium4.0版本中就开始全面支持WebView的自动化了。要了解如何使用Robotium测试框架来对App中的WebView部分进行自动化测试,首先需要了解HTML基础,然后了解Robotium是如何获取页面元素并进行操作的。

1)HTML基础

Robotium支持通过ID、className等方式来获取WebElement元素,因此,首先了解ID、className等的概念,模拟打开GitHub首页并查看网页源码如图3-12所示。

HTML元素:指的是从开始标签到结束标签的所有代码。如图3-12所示,Sign in按钮在开始标签<a href="/login" class="btn btn-block primary">与结束标签</a>内,因此整体属于一个HTML元素。

HTML属性:属性总是以名称/值对的形式出现的,比如:name="value"。属性总是在HTML元素的开始标签中规定的。核心属性有class(规定元素的类名)、ID(规定元素的唯一ID)。Sign in按钮中就有class属性,class="btn btn-block primary"。

 

 

图3-12 GitHub首页的HTML结构

2)WebElement相关API及操作

WebElement相关API见表3-9。

表3-9 WebElement相关API

返回值  方法及说明

ArrayList<WebElement> getCurrentWebElements()

获取当前WebView的所有WebElement元素

ArrayList<WebElement> getCurrentWebElements(By by)

通过By根据指定的元素属性获取当前WebView的所有WebElement元素

void      clickOnWebElement(By by)

通过By根据指定的元素属性点击WebElement

void      clickOnWebElement(WebElement webElement)

点击指定的WebElement

void      enterTextInWebElement(By by, String text)

根据by找到WebElement,并输入指定的文本text

boolean waitForWebElement(By by)

等待根据by获得的WebElement出现

 

在Robotium中对WebElement进行操作有两种方式,一种是先获取相应的WebElement,然后发送点击事件,另一种则是直接调用clickOnWebElement(By by)进行点击。

在获取WebElement元素前我们首先需要知道这个页面的HTML结构,需要知道URL链接才能方便地查看HTML元素、属性等。

获取WebView中的页面信息可以参考本书6.3.3节Appium 脚本常见问题及处理方法中如何获取WebView中的页面信息这一部分内容,通过Chrome浏览器中的DevTools工具可以快速方便地查看WebView中的信息。

我们也可以使用原始的如代码清单3-3所示的方式打印出所有的元素信息。

代码清单3-3 使用日志打印方式获取元素信息

ArrayList<WebElement> webElements = solo.getCurrentWebElements();

WebElement webElement = null;

for(int i=0;i< webElements.size();i++){

    webElement = webElements.get(i);

    Log.i("WebElement", "getId:" + webElement.getId());

    Log.i("WebElement","getClassName:"+webElement.getClassName());

    Log.i ("WebElement", "getText:" + webElement.getText());

}

当我们知道了相应页面的元素、属性后,就可以通过元素或属性等信息来获取指定的WebElement。

1)获取当前WebView所有WebElement

ArrayList<WebElement> webElements = solo.getCurrentWebElements();

2)通过className获取

ArrayList<WebElement> signIns = solo.getCurrentWebElements(By

             .className("btn btn-block primary"));

3)通过ID获取

ArrayList<WebElement> signIns = solo.getCurrentWebElements(By

             .id("example_id"));

4)通过textContent获取

ArrayList<WebElement> signIns = solo.getCurrentWebElements(By

             .textContent("Sign in"));

类似的还有通过cssSelector、name、tagName、xpath等方式获取。

5)通过WebElement点击

拿到WebElement后,如果在页面中该标识是唯一的,那么数组长度为1,可以通过clickOnWebElement(WebElement webElement)方法比较精确地点击。

solo.clickOnWebElement(signIns.get(0));

以上获取WebElement并点击也可以直接使用clickOnWebElement(By by)方法完成。

solo.clickOnWebElement(By.className("btn btn-block primary"));

6)WebElement输入

solo.enterTextInWebElement(By.name("userId"), "your username"); 

solo.enterTextInWebElement(By.name("passwd"), "your passwd"); 

同样地,WebElement也支持等待操作,可以通过waitForWebElement(By by)等待相应的元素出现,然后查找,这样可以使脚本更健壮。不过Robotium中的clickOnTx-WebElement(By by)也均默认已经使用了等待机制,因此非特殊情况,脚本中不需要额外增加等待操作。

Robotium中对WebView的支持由于是使用对系统WebView执行JS从而封装获取页面元素的方式,因此该测试框架只支持App中使用系统WebView的情况,如果App或浏览器使用的是非系统内核的WebView,例如腾讯手机QQ浏览器的X5内核,则无法使用,需要引用X5的SDK并对Robotium进行改造才可支持。

4. 断言

自动化测试中,我们获取控件、执行操作后,接下来就是要对操作后的场景进行断言了。Robotium是基于Instrumentation的测试框架,其测试用例编写的框架是基于Junit的,因此,本小节将先介绍Junit中的断言,然后介绍Robotium中适用于Android端自动化的断言。

1)Junit中的断言

Junit中的断言相关API见表3-10。

表3-10 Junit中的断言相关API

返回值  方法及说明

void      assertTrue(String message, boolean condition)

断言传入的condition参数应该为True,否则将抛出一个带有message提示的Throwable异常

void      assertFalse(String message, boolean condition)

断言传入的condition参数应该为False,否则将抛出一个带有message提示的Throwable异常

void      fail(String message)

直接使用例失败,并抛出一个带有message提示的Throawable异常

 

Junit中的断言可以查看Android SDK中junit.framework.Assert包下的Assert类,常用的有assertTrue(String message, boolean condition)方法,即断言方法中第二个参数condition的结果是否为True,如果为True则该语句执行通过,否则该语句将抛出Throwable的异常,而异常中的提示语将为第一个参数message。因此,使用断言时,应该准确明了地说明message参数,以便断言不符合预期时可以快速判断是什么原因导致的。例如断言某个控件应该要显示在界面中,代码如下:

Button loginBtn = (Button) solo.getView("loginBtn");

assertTrue("‘登录’按钮应该要显示在界面", loginBtn.isShown());

同样地,还有assertFalse(String message, boolean condition)方法,用于断言第二个条件中的结果应该为False。通过这两个方法,只要测试过程中的预期结果能转换成True或False的都可以进行判断,例如判断界面元素是否显示、数值大小比较、文本对比等。

在测试工程中,当出现某种场景时,有时我们希望直接使用例失败而不再往下执行,此时可以使用Assert类中的fail(String message)方法,例如:

if(isBadHappened()){

     fail("this should no happened");

}

而如果出现某种场景,我们希望直接使用例通过而不再执行,则此时在用例脚本中直接使用return即可。

2)Robotium中的断言

Robotium中的断言相关API见表3-11。

表3-11 Robotium中的断言相关API

返回值  方法及说明

void      assertCurrentActivity(String message, String name)

断言当前界面是否为name参数指定的Activity,若不是则抛出一个带有message提示的Throwable异常

void      assertMemoryNotLow()

断言当前是否处于低内存状态

 

Robotium基于Junit中的断言判断,也封装了几个方便在Android端自动化时使用的断言方法。例如assertCurrentActivity(String message, String name)方法可以判断当前界面是否是预期的Activity,我们知道Android中许多页面都对应于一个Activity,当App跳转到一个界面时,就可以使用该方法来判断是否已跳转到相应Activity了。

//获取当前的Activity名

String currentActivity =

              solo.getCurrentActivity().getClass().getSimpleName();

// expectedActivity为期望跳转的Activity

solo.assertCurrentActivity("expected xxxActivity" + " but was " + currentActivity, expectedActivity);

另外,测试框架中的assertMemoryNotLow()方法可以用来判断当前是否处于内存吃紧的情况。在Robotium封装的断言API并不多,因为如前文所说,大多数场景都可以使用True或False来进行判断。

3)Android中的断言

Android中的断言相关API见表3-12。

表3-12 Android中的断言相关API

返回值  方法及说明

void      assertOnScreen(View origin, View view)

断言view是否在屏幕中

void      assertBottomAligned(View first, View second)

断言两个view是否底端对齐,即它们的底端y坐标相等

 

在Android SDK中,android.test.ViewAsserts包下有个ViewAsserts类可以方便地进行与控件相关的断言。例如断言控件是否在窗口中assertOnScreen(View origin, View view),断言两个控件是否底部对齐assertBottomAligned(View first, View second),是否右对齐assertRightAligned(View first, View second),等等。而之所以能实现这些断言在于View控件本身就具有非常多的可以用于判断自身状态的属性,例如View可以判断自身是否显示isShown(),判断是否被选中isSelected(),还可以获取自身所在的坐标位置getLocationOnScreen(int[] location)和宽高getWidth()、getHeight(),等等。由于基于Robotium编写的测试用例是以App形式安装进手机的,且运行时是运行在被测应用所在的进程,因此我们使用断言时,可以借助Android SDK中丰富的类库来进行各种判断,例如判断当前网络状态、应用安装情况、当前应用是否处于前台等,可以很方便地对测试的预期结果进行判断。如代码清单3-4所示,调用Android中的API根据包名判断是否是系统应用。

代码清单3-4 根据包名判断是否是系统应用

/**

 * 根据packageName判断该应用是否是系统应用

 * @param packageName  应用的包名

 * @return  true,系统应用;false,非系统应用

 */

public boolean isSystemApp(String packageName){    

    PackageManager pm = getInstrumentation().getTargetContext().getApplicationContext().getPackageManager();

 

    ApplicationInfo applicationInfo = null;

 

    try {

        applicationInfo = pm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);

        if(applicationInfo !=null &&  (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) ==1){

        LogUtils.logD(TAG, "applicationInfo flag:" + (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM));

            return true;

        }

 

    } catch (NameNotFoundException e) {

    }      

   

    return false;

}

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:

华章出版社

官方博客
官网链接