【腾讯TMQ】5小时搞定谷歌原生自动化框架UiAutomator1.0
前言
谷歌对UI测试(UI Tetsting)的概念是:确保用户在一系列操作过程中(例如键盘输入、点击菜单、弹出对话框、图像显示以及其他UI控件的改变),你的应用程序做出正确的UI响应。
UI测试(功能测试、黑盒测试)的好处是不需要测试者了解应用程序的内部实现细节,只需要知道当执行了某些特定的动作后是否会得到其预期的输出。这种测试方法,在团队合作中可以更好地分离的开发和测试角色。然而常见的UI测试多是以手动方式去执行,然后去验证程序是否达到的预期的效果,很显然这种方法耗时、繁琐并且很容易出错。因此我们需要一种可靠的方法来进行UI测试,通过测试框架,我们可以完成针对具体使用场景的测试用例,然后可以循环的、自动的来运行我们的测试case。
所以谷歌推出了下面的UI自动化测试框架
初探
在Android的SDk提供了以下的工具来支持我们进行UI自动化测试:
uiautomatorviewer:一个用来扫描和分析Android应用程序的UI控件的GUI工具。
uiautomator:一个包含创建测试、执行自动化测试API的java库。(照例送上谷歌Uiautomator文档:http://android.toolib.net/tools/help/uiautomator/index.html )
要使用这些工具,你必须安装Android开发工具以下版本:
Android SDK Tools:API 21 版本或者21以上版本;
Android SDK Platform:API 16 版本或者16以上版本.
Uiautomator测试框架的工作流程
下面是自动UI测试所需的步骤的简短概述:
1、安装待测应用到手机,通过uiautomatorviewer分析应用程序界面的控件,并确保应用程序的控件可以被自动化框架访问。
2、创建自动化测试用例来模拟你和应用程序之间交互的步骤。
3、将测试用例编译成一个JAR文件,并发动到应用程序安装的那台测试设备上。
4、运行测试,查看测试结果。
5、修改测试过程中发现的bug。
分析控件
在你开始写测试用例之前,使用uiautomatorviewer可以帮助你熟悉你的UI组件(包括视图和控件)。你可以使用它对当前连接到你电脑上的手机屏幕进行一个快照,然后可以看到手机当前页面的层级关系和每个控件的属性。利用这些信息,你可以写出针对特定UI控件的测试用例。
在 ..\sdk\tools\ 目录下打开 uiautomatorviewer.bat (打开前请手机连接电脑)
想必大家看了上面的动态图,基本上已经了解了一些用法了吧,我再进一步说明一下:
1、 获取快照:
当你要分析一个页面时,首先将手机的页面停留在你要分析的页面,然后用数据线连接电脑。然后点击uiautomatorviewer左上角的第二个图标按钮 Device Screenshot,点击之后会将当前手机界面的快照更新到这里来。
2、页面层级:
右上方的整个区域,就是当前页面布局的层级关系。如果对Android五大布局比较熟悉的话,理解这一层应该不是问题。
3、不可用区域:
右上方的整个区域中的第二个按钮Toggle NAF Nodes,按下后出现的黄色区域代表,这些控件是不被Uiautomator工具识别,无法获取到这些控件的实例。
以QQ首页为例。
我们可以看到,当按下该按钮的时候,下方的三个tab出现黄色区域,这就代表这三个区域的控件,如果你想通过Uiautomator提供的API来获得他们的属性,或者对其进行点击操作,是做不到的,因为你没办法拿到这些控件的实例。
4、属性详情:
右下方的整个区域,是当前选中的页面或者是控件的属性信息。这部分比较重要,我们以后写代码的时候就是需要通过查看属性中的控件的id或者是text等来获取控件的实例,然后点击操作它。
以QQ左上角的头像控件为例:
点击左上角的头像控件之后,右下方区域就会显示这个控件的详细信息。比如这里我们可以得知它的resource-id就是com.tencent.mobileqq:id/conversation_head。
然后利用Uiautomator的API方法就可以得到该控件的实例。
`// 通过id来创建出UiSelector 对象
UiSelector = new UiSelector().resourceId("com.tencent.mobileqq:id/conversation_head");
// 通过UiSelector 对象 创建出 UiObject 对象
UiObject switcher = new UiObject(uiSelector );
// 判断该控件是否存在
if (switcher.exists())
{
//点击该控件
switcher.click();
}`
上面的方法就是知道了该控件的id之后,模拟点击该控件的过程,当然Uiautomator还提供了根据text来获取控件。
这种点击的方法比起Monkeyrunner来说它的好处就是:Monkeyrunner是坐标点击,当一个脚本写好后,换一个分辨率的手机去执行,点击的位置可能就会出错,而Uiautomator点击是先找到该控件,然后再点击该控件,因此可移植性比Monkeyrunner要好;另外代码的易读性也更好一些。
环境搭建
网上关于环境搭建的内容很多,也非常的详细,这里我就简单的说下大体流程和注意事项吧。
1、在Eclipse中建立一个Java的工程。
2、右键选中你建立的工程,在Properties > Java Build Path中:
a、点击 Add Library > JUnit 添加JUnit3/4;
b、点击Add External JARs… 导入 uiautomator.jar and android.jar 这两个jar包。
这里需要注意,导入这两个jar包的时候,注意Android的版本号,后面生成build.xml的时候需要知道你导入的这两个jar是哪个sdk版本的。
3、导入成功之后,就可以写代码了。代码的格式参考下面:
`package com.uia.example.my;
// Import the uiautomator libraries
import com.android.uiautomator.core.UiObject;
import com.android.uiautomator.core.UiObjectNotFoundException;
import com.android.uiautomator.core.UiScrollable;
import com.android.uiautomator.core.UiSelector;
import com.android.uiautomator.testrunner.UiAutomatorTestCase;
public class yourclass extends UiAutomatorTestCase {
public void testDemo() throws UiObjectNotFoundException {
// 测试代码
}
}`
4、使用 android create uitest-project -n %工程名% -t 5 -p %工程目录% 来生存build.xml文件。需要注意的就是 这里的 -t 后面的5 就是Android list后对应的你当初引入两个jar包的sdk版本对应的id。我的是 id: 5 or “android-19”,所以我这里是5
5、生成的build.xml 用ant工具进行编译。编译后会生成“工程名.jar”包(注意这里需要使用 ant build命令来打包,这样有错误可以看到);
6、将该jar包push到手机的 /data/local/tmp 目录下
7、在adb shell 中执行: uiautomator runtest 工程名.jar -c 包名.类名
简单的例子
以一个简单的例子开始吧。我们完成一个 ” 打开QQ,进入QQ空间,然后退出 ” 的case。
代码如下:
`package QQ;
import java.io.IOException;
import com.android.uiautomator.core.UiDevice;
import com.android.uiautomator.core.UiObject;
import com.android.uiautomator.core.UiObjectNotFoundException;
import com.android.uiautomator.core.UiSelector;
import com.android.uiautomator.testrunner.UiAutomatorTestCase;
public class Test_qq extends UiAutomatorTestCase
{
public void testDemo() throws IOException, UiObjectNotFoundException {
// 启应用
Runtime.getRuntime().exec("am start com.tencent.mobileqq/com.tencent.mobileqq.activity.SplashActivity");
sleep(3000);
// 点击 "动态" tab
UiDevice device = getUiDevice();
int height = device.getDisplayHeight();
int width = device.getDisplayWidth();
device.click(width -50, height-50);
sleep(1000);
// 点击 "好友动态" 按钮
UiObject obj_1 = new UiObject(new UiSelector().description("点击进入好友动态"));
obj_1.click();
sleep(2000);
// 点击 左上角返回 "动态"按钮
UiObject obj_2 = new UiObject(new UiSelector().resourceId("com.tencent.mobileqq:id/ivTitleBtnLeft"));
obj_2.click();
sleep(1000);
// 点击菜单键
device.pressMenu();
sleep(1000);
// 点击退出qq
UiObject obj_3 = new UiObject(new UiSelector().text("退出QQ"));
obj_3.click();
sleep(1000);
// 点击确定
UiObject obj_4 = new UiObject(new UiSelector().text("确定"));
obj_4.click();
}
}`
脚本的运行效果如下:
代码详解
针对上面的例子的代码,我对每一句代码都做个详细的解释吧。
第一部分:启动应用
exec() 这个函数的意思,相当于是在你在输入adb shell 命令后,在Android手机系统的命令行下运行。所以上面这句话的意思和我们打开cmd框输入” adb shell am start * ” 是一样的的效果。
一般来说我们做App的自动化的时候,第一步都是把App打开,这个am start命令的就可以帮我们实现,类似与Monkeyrunner API中的startActivity() 函数。
第二部分:点击 “动态” tab
UiDevice对象会在API部分详细讲解,它是一个我们在Uiautomator中经常使用的一个对象。
这里我们首先用它获取到当前手机的宽和高的像素。然后观察到 “动态” tab位于右下方,因此在取得右下角的坐标点后,又进行了一个大概的坐标变化(这里为了简单只是向左和向上移动了50像素,如果要精确的可以进行等比转化),然后点击该坐标。
这里之所以用点击坐标的方法,一方面是因为这个控件Uiautomator不支持用API获得实例(上一节所说的NAF Nodes,如下图),另一方面也是想说明在一些控件没有固定的id、text和desc的时候,我们应该怎么处理。
第三部分:点击 “好友动态”
要想操作一个控件(例如),首先得获得一个UiObject对象,而UiObject对象可以通过UiSelector来构造,而UiSelector可以根据控件的id、text、content-desc来进行构造,这里就是用content-desc来构造。
第四部分:点击左上角返回按钮
同第三部分的方法,找到id后直接获得到UiObject对象,进行点击。
第五部分:点击菜单键
UiDevice 可以模拟点击home、back、menu 这三个键,代码应该大家都懂的怎么变化了吧。
第六部分:退出
这一部分也是先通过获取出控件属性中的text值,然后构造出UiObject对象,完成点击。
以上部分内容就是整个操作QQ这个小例子的全部代码讲解,看完之后对写Uiautomator代码有了更进一步的了解了吧。接下来写看看还有哪些API可以支持我们做更多的事情
API 列举
UiDevice
概述: UiDevice用与访问关设备状态的信息,也可以使用这个类来模拟用户在设备上的操作。可以通过下面的方法得到实例: UiDevice mdevice = getUiDevice();
摘要:
UiSelector
概述: 按照一定的条件(例如控件的text值,资源id),定位界面上的元素。UiSelector对象的最终目的是去构造一个UiObject对象。
摘要: 这里由于篇幅问题只列出根据text构造:
比较常用,准确度也比较高,中文查找的时候,如果遇到 “UiOjbectNotFoundException” 的时候,记得把项目的编码格式改为utf-8。
其他的方式的构造方法也基本上可次方法差不多,大家直接查看API即可。
UiCollection
概述: 篇幅所限,直接参考文档
摘要:http://android.toolib.net/tools/help/uiautomator/UiCollection.html
UiScrollable
概述: 篇幅所限,直接参考文档
摘要:http://android.toolib.net/tools/help/uiautomator/UiScrollable.html
UiObject
概述:可以理解为 直接操作界面ui元素的实例。篇幅所限,直接参考文档
摘要:http://android.toolib.net/tools/help/uiautomator/UiObject.html
如何更高效
到此为止,我们已经了解Uiautomator的基本知识,并且学习了API的用法,因此对于我们来说完成一个UI自动化测试脚本并不难,但是如何将UI自动化应用在实际的项目中,帮我们提高测试的效率呢?本节我们就说说,UI自动化应该怎么去完成。
这里以微信”小视屏”这个功能为例,假设我们要实现一个微信小视频录制的自动化测试的代码。(鉴于隐私原因,默认在执行脚本前,微信已经是登录状态)
分析
当我们要完成一个自动化时,需要考虑这个用例需要怎么设计,需要测试哪些项,怎么验证,出现错误时应该如何处理。
首先需要明确一点,并不是所有需求文档上提到的功能,我们都必须用自动化方式去验证,由于UI自动化本身的局限性,UI自动化的可行度不是100%的准确,因此我们只对“小视屏”的卖点功能进行自动化验证,你也可以理解为对该功能做一个冒烟测试。
小视屏功能的入口一共是三个,分别是下面这三个地方:
我们除了要验证这地方的入口外,还需要在其中一处完成对小视屏的发送,并且验证小视屏发送成功。因此我们可以按照下面流程来进行测试脚本的编写,流程图如下所示:
编码前准备
有了流程图之后,不要迫不及待的编码。编码之前也需要考虑考虑,是否有一些公共的方法可以提取出来做为一个单独的函数呢?
1、点击操作
首先,点击的操作是Uiautomator中用的最多的,而根据控件id和text来做为索引则是更多的。因此我们封装如下的内容:
`/* 定义“通过哪种方式来获得uiselector”的int标识,
如果以后想添加别的方法(例如 通过description 来获取),则可以参考此形式进行扩充 */
final int CLICK_ID = 2000;
final int CLICK_TEXT = 2001;
/* 实现具体的外部可以调用的函数 */
// 通过id来进行点击操作
public boolean ClickById(String id)
{
return ClickByInfo(CLICK_ID, id);
}
// 通过text来进行点击操作
public boolean ClickByText(String text)
{
return ClickByInfo(CLICK_TEXT, text);
}
/* 封装出通用的点击方法,供上面的public函数调用
如果以后想添加别的方法(例如 通过description 来获取),则可以在switch中扩充 */
private boolean ClickByInfo(int CLICK, String str)
{
UiSelector uiselector = null;
// switch根据不同的CLICK标识,创建出UiSelector的对象
switch(CLICK)
{
case CLICK_ID: uiselector = new UiSelector().resourceId(str); break;
case CLICK_TEXT: uiselector = new UiSelector().text(str); break;
default: return false;
}
// 根据UiSelector对象构造出UiObject的对象
UiObject uiobject = new UiObject(uiselector);
// 判断该控件是否存在
if(!uiobject.exists())
{
return false;
}
// 点击
try
{
uiobject.click();
} catch (UiObjectNotFoundException e)
{
e.printStackTrace();
}
return true;
}`
使用上面我的方法封装之后,你只需要调用 ClickByText(“通讯录”); 即可完成对”通信录” 这个控件的点击,并且在因为异常情况获取不到该控件的时候,也不会报出异常。
然而,我们去点击一个控件的时候,当它出现找不到的情况的时候,这有可能就是bug了,我们需要将其记录下来,并且记录下当时的现场,一般采用截图的方法,以便我们查问题时候能更直观的了解到当时机器一个运行情况。因此接下来,我要说说截图和异常处理。
2、截屏和异常处理
上面的代码中,当UiObject对象找不到的时候,我们只是返回了一个false,告诉调用者这次调用失败了,但是为什么失败,怎么避免这样的失败,并没有记录下来。因此在这段代码中,我们需要加以下的内容:
`private boolean ClickByInfo(int CLICK, String str)
{
....
// 判断该控件是否存在
if(!uiobject.exists())
{
TakeScreen(getUiDevice(), str+"-not-find");
return false;
}
....
}
/* 保存屏幕截图
参数descrip 为 描述该截图的内容 */
public void TakeScreen(UiDevice device, String descrip)
{
// 取得当前时间
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
String datestr = calendar.get(Calendar.HOUR_OF_DAY) + "_" +
calendar.get(Calendar.MINUTE) + "_" + calendar.get(Calendar.SECOND);
// 保存文件
File files = new File("/mnt/sdcard/"+datestr+"_"+descrip+".jpg");
device.takeScreenshot(files);
}`
这样当我们在调用 ClickByText(“通讯录”); 找不到控件的时候,我们的脚本就会自动截取当时屏幕的图像保存在我们的手机中(如下图),这样我们只需打开图片,就知道当时发生了什么,为什么没有找到该控件。
看似完美的方案,其实在实际运行中只是帮我们记录了这个控件这一时刻点击失败的原因,而我们想要的是,脚本在调用了这个方法后,尽最大的可能帮我们点击成功。举一个简单的例子:
这是我们写脚本中经常遇到的一个问题,我们需要 ‘在A页面上点击“进入”按钮,跳转到B页面,然后点击B页面上的“保存”按钮’ 完成我们的操作。
一般我们的写法是:
`ClickByText("进入");
ClickByText("保存");`
然而当我们的手机特别卡,或者是页面承载太多东西的时候,当你调用了点击“进入”按钮后,B页面没有及时的跳转出来,这个时候调用B页面上的“保存”按钮,就会出现异常,而如果你没有按照我上面的方案去实现的话,系统就会抛出异常,而使用了我上面的方案之后,系统虽然不会抛出异常,而且会在你找不到B页面的“保存”按钮时截取当前的屏幕,你完全可以根据截图来判断出来:当是没有找到“保存”按钮的原因是,当时的B页面还没有跳转出来。然而在这个时候,我最希望的并不是看到日志告诉我说哪里哪里失败了,而是想让这次的点击效果生效。
那么怎么解决这个问题呢?相信很多亲手写过Uiautomator脚本的朋友都知道,在两个操作直接加如sleep,没错,这是解决方案,那么究竟应该slepp多久呢?因为不同的手机响应时间是不一样的,如果sleep太短就依然存在上述问题;如果sleep太长的话,无疑使得脚本的运行变的缓慢,多出写无用的sleep。因此我们需要去掉if判断的代码,改为在while循环中等待这个控件的出现,一共等待5次,如果到了第五次,它还没有出现的话,那么我们就认为它真的不会出现了,这个时候去截屏比第一次就没有找到更加的有意义。当然如果你还想提高你的UI自动化的健壮性,那么这里还可以加一个类似这样的函数:
`/* 封装出通用的点击方法,供上面的public函数调用
如果以后想添加别的方法(例如 通过description 来获取),则可以在switch中扩充 */
private boolean ClickByInfo(int CLICK, String str)
{
....
// 判断该控件是否存在
int i = 0;
while (!uiobject.exists() && i<5)
{
SolveProblems();
sleep(500);
if (i== 4)
{
TakeScreen(getUiDevice(), str+"-not-find");
return false;
}
i++;
}
....
}
/**
* 当进不下去的时候,使用该方法,例如可能是出现了一些对话框遮挡,该方法会把对话框干掉*/
private void SolveProblems()
{
....
}`
这个 SolveProblems() 函数主要是用来解决一些“麻烦”的,例如我们在操作地图的时候,当gps信号不好的时候,就会弹出下面的对话框:
由于出现的对话框,遮挡住了我们的Activity,影响我们对界面上ui元素的获取,这个时候,我们就可以在SolveProblems() 加入这样一断逻辑:当出现“开启gps”对话框的时候,就点击“残忍的拒绝”,将此对话框给关掉,这样while的判断条件再次执行的时候,就可以成功获取到你想要的元素。
所以说这个SolveProblems()才是提高UI自动化成功率的关键,因为每个App都有自己的特征,因此这部分的内容,需要你们在平时的日积月累中才能总结出来,当你有了一个足够多的经验库之后,你的App几乎不会再因为外界因素而导致失败了。经过我自己在我项目上的尝试,效果非常的显著。
3、日志
日志的重要性不言而喻,当我们在自动化执行的过程中,肯定不会一直盯着屏幕观察,因此日志使我们最依靠的东西。关于日志的记录方法多种多样,我这里提供下我是怎么在Uiautomator中打印日志的:
`public String m_logpathString = "/mnt/sdcard/PerformanceLog.txt";
public void UiAutomationLog(String str)
{
// 取得当前时间
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
String datestr = calendar.get(Calendar.HOUR_OF_DAY) + ":" +
calendar.get(Calendar.MINUTE) + ":" +
calendar.get(Calendar.SECOND) + calendar.get(Calendar.MILLISECOND) + ":";
FileWriter fwlog = null;
try
{
fwlog = new FileWriter(m_logpathString, true);
fwlog.write(datestr + str + "\r\n");
System.out.println(datestr + str);
fwlog.flush();
} catch (IOException e)
{
e.printStackTrace();
} finally
{
try
{
fwlog.close();
} catch (IOException e)
{
e.printStackTrace();
}
}
}`
接下来就是把这个函数加在一些关键的地方,当出错的时候,方便我们排查问题即可。
总结
将上面的代码全部整理之后,我们可以放到一个单独的类中,这样将测试脚本和帮助处理其他功能的脚本进行分离,这样可以更加便捷我们维护测试代码。其次这样写出来的代码可读性高,并且会随着时间的增加,容错性越来越强,最终将行成一个文档的UI自动化测试框架。
本章完~