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;
}