Android自动化测试技巧

解放程序猿宝贵的右手(或者是左手)

——Android自动化测试技巧


Google大神镇楼 : http://developer.android.com/tools/testing-support-library/index.html#UIAutomator

前言:
觉得文章太长不想往后翻的朋友,你们会后悔的,当然,你也可以选择先看后面的,你会觉得很爽,但是相信我,你还是会回来看前面的。那么,还是慢慢往后翻吧。

导入:
人们懒的走路,才创造了汽车;
人们懒的爬楼,才创造了电梯;
人们懒的扫地,才创造了自动扫地机器人。
人类的进步,离不开这些喜欢偷懒的人,现在,程序猿将偷懒上升到了一个新的高度——利用程序来进行自动化软件测试,将测试工程师从繁琐的测试用例中解脱出来,从此可以一边喝着咖啡,一边看着程序自动测试,不必看着测试用例重复无数次的测试步骤,也不必担心操作失误而导致不必要的错误,更不用担心压力测试而导致的身心俱疲。想了解程序猿是如何实现自动化测试的吗,这里有你想要的答案。

声明
转载真的请注明出处:
http://blog.csdn.net/eclipsexys

顺便打个广告:
我的慕课网视频: http://www.imooc.com/space/teacher/id/347333

为啥要测试

  • 发现错误、为程序员提供修改意见
  • 验证软件是否满足设计需求和技术需求
  • 验证生产环境下真实的用户使用过程,分析用户体验

——总而言之一句话——软件测试,决定着软件的质量。

以前在TCL的时候,每个软件版本都要不停的跑MonkeyTest,一个是检测系统ROM的稳定性,一个是检测各种第三方应用在ROM上的使用情况,所以经常会报出很多Monkey跑出来的Bug,这些Bug经过我们分析,会初步判断是第三方App的问题还是系统的ROM问题,如果是第三方的问题,我们也会提交给App的运营商,但是大部分的运营商给我们的回复都是,我们的App不支持跑Monkey,其实Monkey可以发现一些潜在的问题,特别是一些很难复现的问题,我以前的leader曾经说过一句话我觉得非常好,没有什么bug是不能复现的,没有复现,只是没有找到必先的步骤,所以每一个bug都不是偶然的,我们应该尽量严谨的分析每一个可能存在的bug。


再以前的时候,对日的公司对测试更是无比看重,各种UT测试式样书,不仅仅是要写好怎么测试、测试什么,而且测试的数据、中间过程还要截图,保留证据。


有哪些测试

  • Google CTS测试:兼容性测试,测试ROM的兼容性标准
  • Google GTS测试
  • 实验室机器人测试、机械臂自动化模拟测试
  • Monkey Test压力测试
  • End User终端用户测试

对于美国的手机运营商,例如T-Mobile、Sprite、AT&T,他们都有一系列的手机性能测试,他们的测试项目、测试方法、测试过程,其实都是他们的商业机密,一个是保证测试结果的严谨性,一个也保证了手机厂商能够不作弊的完成测试,所以,千万不要学华X手机,在T-Mobile实验室偷拍手机测试机器人的软件、技术参数及其他机密信息,而被T-Mobile列入北美黑名单。逗比新闻


Android自动化测试工具


自动化测试是把以人为驱动的测试行为转化为机器执行的一种过程


  • 将大量重复的测试步骤用脚本代替,让机器完成重复工作
  • 规范测试用例,保证测试质量
  • 高——大——上

自动化测试的工具


  • MonkeyRunner
    monkeyrunner工具提供一个API来控制Android设备。可以写一个python脚本来安装应用,运行应用,发送键值,截图。monkeyrunner对python进行了封装,加入了一些针对Android设备的类。可以完全用python脚本来实现这些功能。

  • Instrumentation
    基于Android单个Activitiy的测试框架。

  • Robotium
    一个优秀的测试框架,基于Instrumentation的二次封装。

  • QTP
    一个Web上的自动化测试工具,通过录制脚本来实现自动化测试。

  • UiAutomator
    目前最佳的UI自动化测试框架。基于Android 4.X+系统,专业UI自动化测试,可以模拟用户对手机的各种行为。编写快速、可以使用大部分的Android API、无需签名,无任何Activity限制。



各个测试框架的优缺点如下表所示:
测试框架 | 使用语言 | 运行方式 | 限制 | 适用环境
-------- | ---
MonkeyRunner| Python | ADB、Python | 测试靠坐标 | 压力测试
Instrumentation | Java | ADB | 只能单个Activity测试,且需要应用相同签名,代码量大 | 白盒测试
Robotium | 同上 | 同上 | 同上 | 同上
UiAutomator | Java | ADB或者脱机| Android 4.X+| UI测试



综上所述,我们使用UiAutomator作为我们Android自动化测试的首选框架。


UiAutomator环境搭建


开发环境:eclipse(非常抱歉,还没学会如何使用AS来开发Java代码、进行jar打包,请了解的朋友留言!!!)
编译环境:Ant、Java、Android SDK


UiAutomator基本对象之UiDevice


通常用于获取系统的设备信息、系统按键、全局操作等。

获取坐标参数

返回值 | 方法 | 解释
-------- | ---
boolean| click(int x, int y) | 在点(x, y)点击
int | getDisplayHeight() | 获取屏幕高度
int | getDisplayWidth() | 获取屏幕宽度
Point | getDisplaySizeDp() | 获取显示尺寸大小

系统信息

返回值 | 方法 | 解释
-------- | ---
void | getCurrentPackageName() | 获取当前界面包名
void | getCurrentActivityName() | 获取当前界面Activity
void | dumpWindowHierarchy(fileName) | dump当前布局文件到/data/local/tmp/目录

滑动、拖拽

返回值 | 方法 | 解释
-------- | ---
boolean | drag(startX, startY, endX, endY, steps)| 拖拽坐标处对象到另一个坐标
boolean | swipe(segments, segmentSteps) | 在Points[]中以segmentSteps滑动
boolean | swipe(startX, startY, endX, endY, steps) | 通过坐标滑动

系统按键

返回值 | 方法 | 解释
-------- | ---
void | wakeUp() | 按电源键亮屏
void | sleep() | 按电源键灭屏
boolean | isScreenOn() | 亮屏状态
void | setOrientationLeft() | 禁用传感器,并左旋屏幕,固定
void | setOrientationNatural() | 禁用传感器,恢复默认屏幕方向,固定
void | setOrientationRight() | 禁用传感器,并右旋屏幕,固定
void | unfreezeRotation() | 启用传感器,并允许旋转
boolean | isNaturalOrientation() | 检测是否处于默认旋转状态
void | getDisplayRotation() | 返回当前旋转状态,0、1、、2、3分别代表0、90、180、270度旋转
void | freezeRotation() | 禁用传感器,并冻结当前状态
boolean | takeScreenshot(storePath) | 当前窗口截图、1.0f缩放、90%质量保存在storePath
void | takeScreenshot(storePath, scale, quality) | 同上,但指定缩放和压缩比率
void | openNotification() | 打开通知栏
void | openQuickSettings() | 打开快速设置

等待窗口

返回值 | 方法 | 解释
-------- | ---
void | waitForIdle() | 等待当前窗口处于空闲状态、默认10s
void | waitForIdle(long timeout) | 自定义超时等待当前窗口处于空闲状态
boolean | waitForWindowUpdate(packageName, timeout) | 等待窗口内容更新

示例代码

// 输入按键
UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_A);
UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_B);
UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_C);
UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_A,1);
UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_B,1);
UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_C,1);

// 点击
UiDevice.getInstance().click(400, 400);
int h=UiDevice.getInstance().getDisplayHeight();
int w=UiDevice.getInstance().getDisplayWidth();
UiDevice.getInstance().click(w/2, h/2);

// Swipe、Drag
int startX, startY, endX, endY, steps;
startX=300;
startY=400;
endX=startX;
endY=startY + 200;
steps=100;
UiDevice.getInstance().drag(startX, startY, endX, endY, steps);

int h=UiDevice.getInstance().getDisplayHeight();
int w=UiDevice.getInstance().getDisplayWidth();
UiDevice.getInstance().swipe(w, h/2, 30, h/2, 10);

Point p1=new Point();
Point p2=new Point();
Point p3=new Point();
Point p4=new Point();

p1.x=250;p1.y=300;
p2.x=600;p2.y=350;
p3.x=800;p3.y=800;
p4.x=200;p4.y=900;

Point[] pp={p1,p2,p3,p4};

UiDevice.getInstance().swipe(pp, 50);

// 灭屏、亮屏
UiDevice.getInstance().sleep();
UiDevice.getInstance().wakeUp();

// Notification
UiDevice.getInstance().openNotification();
sleep(3000);
UiDevice.getInstance().openQuickSettings();

UiDevice.getInstance().dumpWindowHierarchy("ui.xml");

送个视频,让大家真实体验下:

<iframe height=498 width=510 src="http://player.youku.com/embed/XOTUzMjI2NzYw" frameborder=0 allowfullscreen></iframe>

视频代码:

        UiDevice.getInstance().pressBack();
        UiDevice.getInstance().pressBack();
        UiDevice.getInstance().pressHome();
        sleep(1000);
        UiDevice.getInstance().pressMenu();
        sleep(1000);
        UiDevice.getInstance().pressBack();
        sleep(1000);
        UiDevice.getInstance().pressRecentApps();
        sleep(1000);
        UiDevice.getInstance().pressHome();
        sleep(1000);
        UiDevice.getInstance().click(240, 1100);
        sleep(2000);
        UiDevice.getInstance().click(670, 1100);
        sleep(2000);
        UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_H);
        sleep(1000);
        UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_H, 1);
        sleep(1000);
        UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_J);
        sleep(1000);
        UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_J, 1);
        sleep(1000);
        UiDevice.getInstance().swipe(30, 400, 600, 400, 10);
        sleep(1000);
        UiDevice.getInstance().pressHome();
        sleep(1000);
        UiDevice.getInstance().drag(660, 860, 360, 360, 50);
        sleep(1000);
        UiDevice.getInstance().sleep();
        sleep(1000);
        UiDevice.getInstance().wakeUp();
        sleep(1000);
        UiDevice.getInstance().swipe(370, 1000, 370, 200, 50);
        sleep(1000);
        UiDevice.getInstance().takeScreenshot(new File("/sdcard/uidevice.png"));

UiAutomator基本对象之UiSelector


通常使用UiSelector,通过各种属性节点和关系来定位组件,类似SQL语句的where条件。

uiautomatorviewer

要查看界面UI元素的层级关系,我们需要使用SDK/tools/下面的uiautomatorviewer工具来帮助我们进行查看,运行uiautomatorviewer,点击dump,我们就可以获取当前界面的UI快照。
下面这张图就是一个示例:


uiautomatorviewer

通过uiautomatorviewer,我们可以找到很多对象的属性,上图右下角的方框中的,都是对象所具有的属性。我们可以通过这些属性来定位需要的元素对象,这里要注意的是,uiautomator可以使用链式查找,即一个条件无法定位,那么可以通过多个条件组合,来定位一个元素。

通过text、description属性定位

返回值 | 方法 | 解释
-------- | ---
UiSelector | text(text) | 通过text完全定位
UiSelector | textContains(text) | 通过text包含定位
UiSelector | textMatches(regex) | 通过text正则定位
UiSelector | textStartsWith(text) | 通过text起始文字定位
UiSelector | description(text) | 通过text完全定位
UiSelector | descriptionContains(text) | 通过description包含定位
UiSelector | descriptionMatches(regex) | 通过description正则定位
UiSelector | descriptionStartsWith(text) | 通过description起始文字定位

通过resourceId定位

返回值 | 方法 | 解释
-------- | ---
UiSelector | resourceId(id) | 通过resourceId定位
UiSelector | resourceIdMatches(regex) | 通过resourceId正则定位

通过class、package定位

这种方式适用于当前页面上只有一种类型的组件的情况,例如只有一个ListView。
返回值 | 方法 | 解释
-------- | ---
UiSelector | className(className) | 通过class定位
UiSelector | classNameMatches(regex) | 通过class正则定位
UiSelector | packageName(name) | 通过package定位
UiSelector | packageNameMatches(regex) | 通过package正则定位

通过index、instance定位

返回值 | 方法 | 解释
-------- | ---
UiSelector | index(index) | 通过index定位
UiSelector | instance(instance) | 通过instance定位

通过其它属性定位

返回值 | 方法 | 解释
-------- | ---
UiSelector | enabled(val) | 通过enabled属性定位
…… | …… | ……
对象的所有属性都可以使用,这里不再列举。

示例代码

// 找到对象 点击对象
UiSelector l=new UiSelector().text("联系人");
UiObject object=new UiObject(l);
object.click();

// 匹配方式
// 完全匹配:联系人
// 包含匹配:系人
// 正则匹配:.*系.*
// 起始文字匹配:联系

UiSelector l=new UiSelector().textContains("系人");
UiSelector l=new UiSelector().textMatches(".*系.*");
UiSelector l=new UiSelector().textStartsWith("联系");
UiObject object=new UiObject(l);
object.click();

UiAutomator基本对象之UIObject


UIObject是UiAutomator的核心属性之一。它代表了整个UI界面中的所有对象元素。
它的功能包括:获取UI元素,点击、拖拽、滑动、对象属性判断、手势等。

点击与长按

返回值 | 方法 | 解释
-------- | ---
boolean | click() | 点击对象
boolean | clickAndWaitForNewWindow() | 点击对象并等待新窗口出现
boolean | clickAndWaitForNewWindow(timeout) | 点击对象并等待新窗口出现,指定延迟
boolean | clickBottomRight() | 点击对象右下角
boolean | clickTopLeft() | 点击对象左上角
boolean | longClick() | 长按对象
boolean | longClickBottomRight() | 点击对象右下角
boolean | longClickTopLeft() | 点击对象左上角

拖拽与滑动

返回值 | 方法 | 解释
-------- | ---
boolean | dragTo(destObj, steps) | 以steps拖动对象到destObj
boolean | dragTo(destX, destY, steps) | 以steps拖动对象到坐标
boolean | swipeDown(steps) | 向下拖动
boolean | swipeLeft(steps) | 向左拖动
boolean | swipeRight(steps) | 向右拖动
boolean | swipeTop(steps) | 向上拖动

文本输入与清除

返回值 | 方法 | 解释
-------- | ---
boolean | setText(text) | 设置内容为text
boolean | clearTextField() | 清除文本

获取对象属性

返回值 | 方法 | 解释
-------- | ---
Rect| getBounds() | 获取对象矩形范围
int | getChildCount() | 获取子View数量
……| …… | ……
还有很多,不列举了。

获取对象属性状态

返回值 | 方法 | 解释
-------- | ---
boolean | isCheckable() | 获取对象checkable状态
……| …… | ……
还有很多,不列举了。

获取对象存在状态

返回值 | 方法 | 解释
-------- | ---
boolean | waitForExists(timeout) | 等待对象出现
boolean | waitUntilGone(timeout) | 等待对象消失
boolean | exists() | 对象是否存在

手势状态

返回值 | 方法 | 解释
-------- | ---
boolean | performMultiPointerGesture(touches) | 执行单指手势
boolean | performTwoPointerGesture(startPoint1, startPoint2, endPoint1, endPoint2, steps) | 执行双指手势
boolean | pinchIn(percent, steps) | 双指向内收缩
boolean | pinchOut(percent, steps) | 双指向外张开

示例代码

// 拖拽
UiObject object1=new UiObject(new UiSelector().text("联系人"));
UiObject object2=new UiObject(new UiSelector().text("图库"));
object1.dragTo(300,1200, 10);
object1.dragTo(object2, 30);
object1.swipeUp(5);

// 输入、清空
UiObject edit=new UiObject(new UiSelector()
.resourceId("com.hjwordgames:id/edit_password"));

edit.setText("xuyisheng");
sleep(2000);
edit.clearTextField();

// 判断
UiObject wlan=new UiObject(new UiSelector()
       .resourceId("com.android.settings:id/switchWidget"));
       
if(!wlan.isChecked()){
    wlan.click();  
}

// 手势
UiObject object=new UiObject(new UiSelector()
    .resourceId("com.android.gallery3d:id/photopage_bottom_controls"));
    
object.pinchIn(80, 20);
object.pinchOut(80, 20);
    
Point startPoint1, startPoint2, endPoint1, endPoint2;
startPoint1=new Point();
startPoint2=new Point();
endPoint1=new Point();
endPoint2=new Point();
    
startPoint1.x=150;startPoint1.y=200;
startPoint2.x=100;startPoint2.y=500;
    
endPoint1.x=900;endPoint1.y=200;
endPoint2.x=950;endPoint2.y=500;

object.performTwoPointerGesture(startPoint1, startPoint2, endPoint1, endPoint2, 50);

再送一个视频、不收费:

<iframe height=498 width=510 src="http://player.youku.com/embed/XOTUzMjI3NDk2" frameborder=0 allowfullscreen></iframe>

视频代码:

        UiObject word = new UiObject(new UiSelector().text("沪江开心词场"));
        word.clickAndWaitForNewWindow();
        UiObject username = new UiObject(new UiSelector().text("沪江用户名/邮箱/手机"));
        username.setText("xuyisheng");
        sleep(1000);
        UiObject pwd = new UiObject(
                new UiSelector().resourceId("com.hjwordgames:id/edit_password"));
        pwd.setText("123465");
        sleep(2000);
        pwd.clearTextField();
        sleep(1000);
        pwd.setText("123465");
        UiDevice.getInstance().pressBack();
        sleep(1000);
        UiObject login = new UiObject(new UiSelector().text("登 录"));
        login.clickAndWaitForNewWindow();
        UiDevice.getInstance().pressBack();
        UiDevice.getInstance().pressBack();
        word.dragTo(300, 300, 50);
        sleep(1000);
        word.swipeDown(50);

UiAutomator基本对象之UIScrollable


专业处理滚动一百年。

滚动

返回值 | 方法 | 解释
-------- | ---
boolean | flingBackward() | 步长为5快速向后滑动
boolean | flingForward() | 步长为5快速向前滑动
boolean | flingToBeginning(maxSwipes) | 不超过maxSwipes滑动到最前,步长为5
boolean | flingToEnd(maxSwipes) | 不超过maxSwipes滑动到最后,步长为5
boolean | flingToEnd(maxSwipes) | 不超过maxSwipes滑动到最后,步长为5
……| …… | 同样还可以使用Scroll,不一一列举

获取列表子元素

返回值 | 方法 | 解释
-------- | ---
boolean | getChildByDescription(childPattern, text) | 默认滚动,查找childPattern UiSelector所对应的text子元素
boolean | getChildByDescription(childPattern, text, allowScrollSearch) | 是否允许滚动,查找childPattern UiSelector所对应的text子元素
…… | …… | 还有text、instance同样可以使用,不一一列举。
boolean | scrollIntoView(obj) | 滚动到obj所处的位置
boolean | scrollIntoView(selector) | 滚动到条件元素所处的位置
boolean | scrollTextIntoView(text) | 滚动到文本对象所处的位置
boolean | scrollToBeginning(maxSwipes) | 滚动到开始位置
boolean | scrollToBeginning(maxSwipes, steps) | 指定步长,滚动到开始位置
boolean | scrollToEnd(maxSwipes) | 滚动到最后位置
boolean | scrollToEnd(maxSwipes, steps) | 指定步长,滚动到最后位置
boolean | setMaxSearchSwipes(swipes) | 设置最大可扫动次数
boolean | getMaxSearchSwipes() | 获取最大可扫动次数、默认30
UiScrollable | setSwipeDeadZonePercentage(swipeDeadZonePercentage) | 设置滑动无效区域(到顶部的百分比)
double | getSwipeDeadZonePercentage() | 获取滑动无效区域(到顶部的百分比)

滚动方向

返回值 | 方法 | 解释
-------- | ---
boolean | setAsHorizontalList() | 设置水平滚动
boolean | setAsVerticalList() | 设置垂直滚动

示例代码

// 滑动
UiScrollable scroll=new UiScrollable(new UiSelector().className("android.widget.ListView"));
scroll.flingBackward();
scroll.flingForward();
scroll.flingToBeginning(20);
scroll.flingToEnd(30);

// 滑动到某元素
UiScrollable scroll=new UiScrollable(new UiSelector().className("android.widget.ListView"));
UiObject baiQiang=scroll.getChildByText(new UiSelector().className("android.widget.TextView"), "zhujia");
baiQiang.click();
       
scroll.getChildByInstance(new UiSelector().className("android.widget.TextView"), 25).click();

// 滑动到某元素
UiScrollable scroll=new UiScrollable(new UiSelector().className("android.widget.ListView"));
UiSelector selector=new UiSelector().text("zhujia");
UiObject object=new UiObject(selector);
scroll.scrollIntoView(selector);
scroll.scrollIntoView(object);
scroll.scrollTextIntoView("zhujia");
scroll.scrollDescriptionIntoView("zhujia");

scroll.scrollToBeginning(50,5);
scroll.scrollToEnd(50,5);

// 滑动方向
UiScrollable scroll=new UiScrollable(new    UiSelector().className("android.support.v4.view.ViewPager"));
scroll.setAsHorizontalList();
scroll.scrollBackward();
sleep(2000);
scroll.scrollForward();
sleep(2000);
scroll.setAsVerticalList();
scroll.scrollForward();

视频大放送:

<iframe height=498 width=510 src="http://player.youku.com/embed/XOTUzMjI3ODIw" frameborder=0 allowfullscreen></iframe>

视频代码:

        UiScrollable scrollable = new UiScrollable(
                new UiSelector().className("android.widget.ListView"));
        scrollable.flingForward();
        sleep(500);
        scrollable.flingBackward();
        sleep(500);
        scrollable.flingForward();
        UiObject target = new UiObject(new UiSelector().text("德国工业就是这么强大!不得不服"));
        scrollable.scrollIntoView(target);
        target.click();

UiAutomator基本对象之UICollection


通常用于获取满足某种搜索条件的组件集合,通过链式搜索确定最终需要的组件。
先按照一定的条件枚举容器内的子元素,再从符合条件的子元素中进一步定位。
一般使用容器类组件作为父类,用于寻找不好定位的子元素。

示例代码

UiCollection collection=new UiCollection(new UiSelector().className("android.widget.ListView"));
UiSelector childPattern=new UiSelector().className("android.widget.TextView");
String text="Music";        
UiObject music=collection.getChildByText(childPattern, text);
music.click();

UiAutomator基本对象之UiWatcher


通常我们会让脚本来按照我们所需要的顺序来执行,但有时候,总有一些天灾人祸,比如10086发短信来了。
所以,我们的脚本必须要有一定的容错性。

UiWatcher正是这样一个容错的对象,当我们在顺序执行脚本时,如果中间突然插入了一些不明事件,我们可以使用UiWatcher来拦截异常,处理完异常后,再返回原来的脚本执行顺序。


UiAutomator基本对象之Configuration


Configuration,自然是对默认操作的配置,通常情况下,我们使用默认的Configuration就足够了,当然,如果你有一些特殊需求,就可以通过Configuration类来设置。它能更改我们前面提到的所有默认属性的设置。包括默认延迟、输入延迟、等待超时等等。


UiAutomator基本对象之查看报告


下面是一个典型的UiAutomator测试报告:

INSTRUMENTATION_STATUS: numtests=1

INSTRUMENTATION_STATUS: stream=

com.hj.autotest.AutoTest:

INSTRUMENTATION_STATUS: id=UiAutomatorTestRunner

INSTRUMENTATION_STATUS: test=testDevice

INSTRUMENTATION_STATUS: class=com.hj.autotest.AutoTest

INSTRUMENTATION_STATUS: current=1

INSTRUMENTATION_STATUS_CODE: 1

INSTRUMENTATION_STATUS: numtests=1

INSTRUMENTATION_STATUS: stream=.

INSTRUMENTATION_STATUS: id=UiAutomatorTestRunner

INSTRUMENTATION_STATUS: test=testDevice

INSTRUMENTATION_STATUS: class=com.hj.autotest.AutoTest

INSTRUMENTATION_STATUS: current=1

INSTRUMENTATION_STATUS_CODE: 0

INSTRUMENTATION_STATUS: stream=

Test results for WatcherResultPrinter=.

Time: 31.489



OK (1 test)





INSTRUMENTATION_STATUS_CODE: -1

这些报告被INSTRUMENTATION_STATUS_CODE分为了三个部分,1表示运行前,-1表示运行完成。

如果出错了,你可以在报告中找到相应的错误信息。

你同样需要知道的是,UiAutomator也是JUnit工程,你同样可以在里面使用断言来进行某些变量、结果值的测试,这些同样会在报告中体现出来。

最后,UiAutomator大部分内容都讲完了,最后一个视频:

<iframe height=498 width=510 src="http://player.youku.com/embed/XOTUzMjI3OTg0" frameborder=0 allowfullscreen></iframe>

视频代码:

        UiDevice.getInstance().pressHome();
        new UiObject(new UiSelector().description("Apps"))
                .clickAndWaitForNewWindow();
        UiScrollable scrollable = new UiScrollable(
                new UiSelector()
                        .resourceId(
                        "com.google.android.googlequicksearchbox:id/apps_customize_pane_content"));
        scrollable.setAsHorizontalList();
        UiObject word = new UiObject(new UiSelector().text("沪江开心词场"));
        while (!word.exists()) {
            scrollable.scrollForward();
        }
        word.clickAndWaitForNewWindow();
        UiObject username = new UiObject(new UiSelector().text("沪江用户名/邮箱/手机"));
        username.setText("xys10086");
        sleep(1000);
        UiObject pwd = new UiObject(
                new UiSelector().resourceId("com.hjwordgames:id/edit_password"));
        pwd.setText("Aa123465");
        sleep(1000);
        UiObject login = new UiObject(new UiSelector().text("登 录"));
        login.clickAndWaitForNewWindow();
        if (new UiObject(new UiSelector().className(
                "android.widget.FrameLayout").index(1)).exists()) {
            new UiObject(new UiSelector().text("注册"))
                    .clickAndWaitForNewWindow();
            new UiObject(
                    new UiSelector()
                            .resourceId("com.hjwordgames:id/registerEditUsername"))
                    .setText("xys100861");
            new UiObject(
                    new UiSelector()
                            .resourceId("com.hjwordgames:id/registerEditPassword"))
                    .setText("Aa123456");
            new UiObject(
                    new UiSelector()
                            .resourceId("com.hjwordgames:id/regiserEditEmail"))
                    .setText("35998151@qq.com");
            new UiObject(new UiSelector().text("确认注册"))
                    .clickAndWaitForNewWindow();
            UiObject ok = new UiObject(
                    new UiSelector().resourceId("com.hjwordgames:id/btnOK"));
            if (ok.waitForExists(500)) {
                ok.clickAndWaitForNewWindow();
            }
        }

如何使用UiAutomator


配置工程环境

在Eclipse中创建一个java工程,并添加platforms文件夹下面的android.jar和uiautomator.jar 两个引用。如下图:

引用jar

创建测试用例

UiAutomator中的测试类都要继承UiAutomatorTestCase,每个测试用例的方法的方法名都要以test开头。如下图:

测试用例

在测试用例的方法中,我们就可以编写测试脚本代码。

生成build.xml文件

在终端中,输入:

android create uitest-project -n <name> -t <android-sdk-ID> -p <path>

这里的android sdk id指的是在终端中,输入android list返回的你使用的sdk的id。
这里还要PS下,一定要配置好环境变量,这是我们后面一键自动化的基础。

例如:

android create uitest-project -n Demo -t 30 -p "F:\EclipseWorkSpace\AutoTest"

如下图:

这里写图片描述

修改build.xml文件

生成的build.xml文件我们还无法直接使用,我们需要修改它的一个属性,打开build.xml文件,将help改为build,如下图:

这里写图片描述
这里写图片描述

打包Jar

使用Ant,我们利用build.xml打包生成jar,命令如下:

ant -buildfile "F:\EclipseWorkSpace\AutoTest"

编译过程如下图:

这里写图片描述

Push Jar包到手机

我们需要将jar包push到手机中的/data/local/tmp/目录才能启动测试。如下图:

adb push "F:\EclipseWorkSpace\AutoTest\bin\Demo.jar" /data/local/tmp/

执行测试用例

在终端中输入启动测试命令(#后如果不指定具体的用例名,则测试所有的方法),如下:

adb shell uiautomator runtest Demo.jar --nohup -c com.hj.autotest.AutoTest#testBrowser
这里写图片描述

到此为止,整个测试用例的测试就全部结束了。

让自动化测试自动起来

看完前面的步骤,相信很多人已经不想再看下去了,好吧,那你们损失大了,所谓自动化测试,就是为了减少人工的操作,像这样反复的编译、修改、push、运行,这跟手动去测试又有什么区别呢?
OK,让自动化再上升一个境界。
我们可以发现,其实这些操作,与我们进行测试一样,也是一些机械动作,ok,那么我们完全可以使用同样的思路——使用脚本来解决这些问题。
我们创建一个脚本工具——UiAutomatorTool,来封装这些机械的步骤。代码非常简单,无非是使用Java调用终端命令,来执行前面的各种操作。
代码如下:

package com.hj.autotest;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class UiAutomatorTool {

    // 工作空间目录
    private static String WORKSPACE_PATH;

    /**
     * 指定自动测试的参数
     *
     * @param jarName
     *            生成jar的名字
     * @param testPackageclass
     *            测试包名+类名
     * @param testFunction
     *            测试方法名,空字符串代表测试所有方法
     * @param androidId
     *            SDK id
     */
    public UiAutomatorTool(String jarName, String testPackageclass,
            String testFunction, String androidId) {
        System.out.println("*******************");
        System.out.println(" --AutoTest Start--");
        System.out.println("*******************");
        // 获取工作空间目录路径
        WORKSPACE_PATH = getWorkSpase();
        System.out.println("自动测试项目工作空间:\t\n" + getWorkSpase());

        // ***********启动测试*********** //
        // 创建Build.xml文件
        creatBuildXml(jarName, androidId);
        // 修改Build.xml文件中的Build类型
        modfileBuild();
        // 使用Ant编译jar包
        antBuild();
        // push jar到手机
        pushJarToAndroid(WORKSPACE_PATH + "\\bin\\" + jarName + ".jar");
        // 测试方法,为空则测试全部方法
        if (androidId.equals("")) {
            runTest(jarName, testPackageclass);
        } else {
            runTest(jarName, testPackageclass + "#" + testFunction);
        }
        // ***********启动测试*********** //
        System.out.println("*******************");
        System.out.println("---AutoTest End----");
        System.out.println("*******************");
    }

    /**
     * 创建build.xml文件
     */
    public void creatBuildXml(String jarName, String androidID) {
        System.out.println("--------创建build.xml 开始---------");
        execCmd("cmd /c android create uitest-project -n " + jarName + " -t "
                + androidID + " -p " + "\"" + WORKSPACE_PATH + "\"");
        System.out.println("--------创建build.xml 完成---------");
    }

    /**
     * 修改build.xml文件位build type
     */
    public void modfileBuild() {
        System.out.println("--------修改build.xml 开始---------");
        StringBuffer stringBuffer = new StringBuffer();
        try {
            File file = new File("build.xml");
            if (file.isFile() && file.exists()) {
                InputStreamReader read = new InputStreamReader(
                        new FileInputStream(file));
                BufferedReader bufferedReader = new BufferedReader(read);
                String lineTxt;
                while ((lineTxt = bufferedReader.readLine()) != null) {
                    if (lineTxt.matches(".*help.*")) {
                        lineTxt = lineTxt.replaceAll("help", "build");
                    }
                    stringBuffer = stringBuffer.append(lineTxt).append("\t\n");
                }
                read.close();
            } else {
                System.out.println("找不到build.xml文件");
            }
        } catch (Exception e) {
            System.out.println("读取build.xml内容出错");
            e.printStackTrace();
        }
        // 重新写回build.xml
        rewriteBuildxml("build.xml", new String(stringBuffer));
        System.out.println("--------修改build.xml 完成---------");
    }

    /**
     * 使用Ant编译jar包
     */
    public void antBuild() {
        System.out.println("--------编译build.xml 开始---------");
        execCmd("cmd /c ant -buildfile " + "\"" + WORKSPACE_PATH + "\"");
        System.out.println("--------编译build.xml 完成---------");
    }

    /**
     * adb push jar包到Android手机
     *
     * @param localPath
     *            localPath
     */
    public void pushJarToAndroid(String localPath) {
        System.out.println("--------push jar 开始---------");
        localPath = "\"" + localPath + "\"";
        System.out.println("jar包路径:" + localPath);
        String pushCmd = "adb push " + localPath + " /data/local/tmp/";
        execCmd(pushCmd);
        System.out.println("--------push jar 完成---------");
    }

    /**
     * 测试方法
     *
     * @param jarName
     *            jar包名
     * @param testName
     *            testName
     */
    public void runTest(String jarName, String testName) {
        System.out.println("--------测试方法 开始---------");
        String runCmd = "adb shell uiautomator runtest ";
        String testCmd = jarName + ".jar " + "--nohup -c " + testName;
        execCmd(runCmd + testCmd);
        System.out.println("--------测试方法 完成---------");
    }

    /**
     * 获取WorkSpace目录
     *
     * @return WorkSpace目录
     */
    public String getWorkSpase() {
        File directory = new File("");
        return directory.getAbsolutePath();
    }

    /**
     * Shell命令封装类
     *
     * @param cmd
     *            Shell命令
     */
    public void execCmd(String cmd) {
        System.out.println("ExecCmd:" + cmd);
        try {
            Process p = Runtime.getRuntime().exec(cmd);
            // 执行成功返回流
            InputStream input = p.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(
                    input, "GBK"));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
            // 执行失败返回流
            InputStream errorInput = p.getErrorStream();
            BufferedReader errorReader = new BufferedReader(
                    new InputStreamReader(errorInput, "GBK"));
            String eline;
            while ((eline = errorReader.readLine()) != null) {
                System.out.println(eline);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 重新写回Build.xml
     *
     * @param path
     *            path
     * @param content
     *            content
     */
    public void rewriteBuildxml(String path, String content) {
        File dirFile = new File(path);
        if (!dirFile.exists()) {
            dirFile.mkdir();
        }
        try {
            BufferedWriter bw1 = new BufferedWriter(new FileWriter(path));
            bw1.write(content);
            bw1.flush();
            bw1.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

那我们怎么使用呢?拿一个测试类来说:

public class AutoTest extends UiAutomatorTestCase {

    public static void main(String[] args) {
        new UiAutomatorTool("Demo", "com.hj.autotest.AutoTest", "testUiSelector", "30");
    }
    ……
}

我们只需要在测试类中new一个UiAutomatorTool,并指定jar包名、包名、用例名、Android id即可。
接下来,只需要运行这样Java程序,就完成了整个过程的自动化,一键编译、一键运行。

好吧,再来一个视频:

<iframe height=498 width=510 src="http://player.youku.com/embed/XOTUzMjcyMDg4" frameborder=0 allowfullscreen></iframe>


让偷懒更进一步


前面我们已经让编译、push、运行自动化了,但是说到底,就连编写脚本也是一件非常繁琐的事情啊。OK,我们同样可以创建一个H5的页面,通过编写图形化的页面,来替代我们每个动作脚本的编写,毕竟这些脚本也是死的啊。

让偷懒发扬光大

这些脚本可不仅仅能做测试。
经过前面一系列的代码、演示,我们已经可以通过脚本来进行测试用例的自动化测试,但是,自动化不仅仅可以用来测试,当我们在调试程序的时候,经常需要登陆App以后才能进行测试,我们同样可以把这些操作放到脚本中,启动调试后,直接运行脚本,完成这样繁琐的输入、登陆步骤。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,799评论 25 707
  • 作者:Ringoyan,腾讯测试开发工程师。先后为植物大战僵尸Online,糖果传奇等游戏担任测试经理,其负责的“...
    饭盒阅读 2,784评论 2 41
  • 不知该如何说起,亦不知该怎样表述才较为妥当,内心五味杂陈。有些事即使你忘了,也会造成阴影,在你不经意间一件类...
    她有刺阅读 364评论 0 0
  • 春雨如酒 醉人心头 翠枝荡漾迎风起 点点竞开且娇羞 年年岁岁 难忘六月初六 青青槐豆 经不起凉风飕飕 熟落的槐豆 ...
    李阿歡阅读 598评论 0 1