版权声明:本文出自胖喵~的博客,转载必须注明出处。

转载请注明出处:http://www.cnblogs.com/by-dream/p/4996000.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。因此我们需要下面的方案解决:

    private boolean ClickByInfo(int CLICK, String str)
{
....
// 判断该控件是否存在
if(!uiobject.exists())
{
TakeScreen(getUiDevice(), str+"-not-find");
return false;
}
int i = 0;
while (!uiobject.exists() && i<5)
{
sleep(500);
if (i== 4)
{
TakeScreen(getUiDevice(), str+"-not-find");
        return false; 
}
i++;
}
....
}

  我们去掉了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() 这个函数的理解:

  所以说这个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();
}
}
}

  接下来就是把这个函数加在一些关键的地方,当出错的时候,方便我们排查问题即可。下面是脚本打出来的日志格式:

实现


  总结上面的所有代码,我们把这些放到一个公共的方法中,这样我们的脚本就可以直接引入这个类,然后直接的进行调用这些函数了。公共方法和测试的脚本我们单独分开,像下面一样:

  首先附上公共方法的完整源代码:

 package QQ;

 import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Calendar; 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 UiautomatorAssistant extends UiAutomatorTestCase
{
/* UiDevice对象*/
UiDevice mdevice; /* log地址 */
public String m_logpathString = "/mnt/sdcard/PerformanceLog.txt"; /* 定义“通过哪种方式来获得uiselector”的int标识,
如果以后想添加别的方法(例如 通过description 来获取),则可以参考此形式进行扩充 */ final int CLICK_ID = 2000;
final int CLICK_TEXT = 2001; /*构造传入UiDevice对象*/
UiautomatorAssistant(UiDevice device)
{
mdevice =device;
} /* 实现具体的外部可以调用的函数 */ // 通过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);
// 判断该控件是否存在
int i = 0;
while (!uiobject.exists() && i<5)
{
SolveProblems();
sleep(500);
if (i== 4)
{
TakeScreen(str+"-not-find");
return false;
}
i++;
}
// 点击
try
{
UiAutomationLog("click type:"+CLICK+" content:"+str );
uiobject.click();
} catch (UiObjectNotFoundException e)
{
e.printStackTrace();
}
return true;
} /* 当进不下去的时候,使用该方法,例如可能是出现了一些对话框遮挡,该方法会把对话框干掉 */
private void SolveProblems()
{ } /* 保存屏幕截图
参数descrip 为 描述该截图的内容 */
public void TakeScreen(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");
UiAutomationLog("TakeScreen: " + datestr+"_"+descrip+".jpg");
mdevice.takeScreenshot(files);
} /* 打log记录在手机中 */
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();
}
}
}
}

  这个类需要注意的就是31行这里的UiDevice对象需要从测试类中传递过来。否则无法得到UiDevice对象。

  好,接下来我们看看最终实现的脚本的源码:

 package QQ;

 import java.io.IOException;

 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_wechat extends UiAutomatorTestCase
{ UiautomatorAssistant uiautomatorAssistant ; public void testDemo() throws IOException, UiObjectNotFoundException { uiautomatorAssistant = new UiautomatorAssistant(getUiDevice()); // 启动App
Runtime.getRuntime().exec("am start com.tencent.mm/com.tencent.mm.ui.LauncherUI");
sleep(3000); /*----------------------- 验证第一种小视频的打开方式------------------------------------*/
uiautomatorAssistant.ClickByText("通讯录");
// 点击一个好友
uiautomatorAssistant.ClickById("com.tencent.mm:id/gx");
// 点击发消息
uiautomatorAssistant.ClickByText("发消息");
// 点击发送栏的“+”
uiautomatorAssistant.ClickById("com.tencent.mm:id/wm");
// 点击小视频
uiautomatorAssistant.ClickByText("小视频");
// 验证第一种小视频打开方式
UiObject obj_anzhupaiObject = new UiObject(new UiSelector().text("按住拍"));
if (obj_anzhupaiObject.exists())
{
uiautomatorAssistant.UiAutomationLog( "第一次进入小视频的方法测试通过");
}
else {
uiautomatorAssistant.TakeScreen("第一次进入小视频的方法测试不通过");
} // 第二种、第三种类似第一种的方法,这里省略。
// 在我代码还没完成的时候,微信发出了新版本,屏蔽了第三种下拉的方式,打开小视频。
} }

  很明显可以看到,使用了封装函数之后,代码的可读性大大的增强,而且很好的维护,要完成一个其他的case也可以轻而易举。后续如果读者朋友有需要完成Uiautomator脚本的不妨可以使用我的公共类来辅助你完成你的脚本,这样你就可以更加高效,快速的完成一个失败率降到最低的UI自动化测试脚本。

【Android测试】【第十三节】Uiautomator——如何组织好你的测试代码(项目实战)的更多相关文章

  1. centos LB负载均衡集群 三种模式区别 LVS/NAT 配置 LVS/DR 配置 LVS/DR + keepalived配置 nginx ip_hash 实现长连接 LVS是四层LB 注意down掉网卡的方法 nginx效率没有LVS高 ipvsadm命令集 测试LVS方法 第三十三节课

    centos   LB负载均衡集群 三种模式区别 LVS/NAT 配置  LVS/DR 配置  LVS/DR + keepalived配置  nginx ip_hash 实现长连接  LVS是四层LB ...

  2. Android进阶(二十八)上下文菜单ContextMenu使用案例

    上下文菜单ContextMenu使用案例 前言 回顾之前的应用程序,发现之前创建的选项菜单无法显示了.按照正常逻辑来说,左图中在"商品信息"一栏中应该存在选项菜单,用户可进行分享等 ...

  3. 第三百二十三节,web爬虫,scrapy模块以及相关依赖模块安装

    第三百二十三节,web爬虫,scrapy模块以及相关依赖模块安装 当前环境python3.5 ,windows10系统 Linux系统安装 在线安装,会自动安装scrapy模块以及相关依赖模块 pip ...

  4. centos LNMP第一部分环境搭建 LAMP LNMP安装先后顺序 php安装 安装nginx 编写nginx启动脚本 懒汉模式 mv /usr/php/{p.conf.default,p.conf} php运行方式SAPI介绍 第二十三节课

    centos  LNMP第一部分环境搭建 LAMP安装先后顺序  LNMP安装先后顺序 php安装 安装nginx  编写nginx启动脚本   懒汉模式  mv   /usr/local/php/{ ...

  5. centos 正则,grep,egrep,流式编辑器 sed,awk -F 多个分隔符 通配符 特殊符号. * + ? 总结 问加星 cat -n nl 输出文件内容并加上行号 alias放~/.bash_profile 2015-4-10 第十三节课

    centos 正则,grep,egrep,流式编辑器 sed,awk -F 多个分隔符  通配符 特殊符号. * + ? 总结  问加星 cat -n  nl  输出文件内容并加上行号 alias放~ ...

  6. (转)第二十三节 inotify事件监控工具

    第二十三节 inotify事件监控工具 标签(空格分隔): Linux实战教学笔记-陈思齐 原文:http://www.cnblogs.com/chensiqiqi/p/6542268.html 第1 ...

  7. 有史来最大改变 Android 5.0十大新特性

    有史来最大改变 Android 5.0十大新特性 2014.10.16 14:51:31 来源:腾讯数码作者:腾讯数码 ( 0 条评论 )   距离Android系统上一次重大更新不到一年的时间,谷歌 ...

  8. 十一、Android学习第十天——项目开始(转)

    (转自:http://wenku.baidu.com/view/af39b3164431b90d6c85c72f.html) 十一.Android学习第十天——项目开始 Android知识点的学习告一 ...

  9. Kali Linux Web 渗透测试— 第二十课-metasploit.meterpreter

    Kali Linux Web 渗透测试— 第二十课-metasploit.meterpreter 原文链接:http://www.xuanhun521.com/Blog/7fc11b7a-b6cb-4 ...

随机推荐

  1. Shell 编程基础之括号的作用

    一.小括号() 单小括号 命令组.括号中的命令将会新开一个子shell顺序执行,所以括号中的变量不能够被脚本余下的部分使用.括号中多个命令之间用分号隔开,最后一个命令可以没有分号,各命令和括号之间不必 ...

  2. WPF文字排列方式解析zz

      WPF文字的处理是一个比较基础的技能.在使用WPF开发工具时,对于各种文字的处理时经常会遇到的情况.希望大家可以通过实践经验的积累,牢固掌握这一方面知识. AD:WOT2014:用户标签系统与用户 ...

  3. ural 1252. Sorting the Tombstones

    1252. Sorting the Tombstones Time limit: 1.0 secondMemory limit: 64 MB There is time to throw stones ...

  4. Storm实战集锦

    一.Kafka+Storm+HDFS整合实践 本文导读: 前言 Kafka安装配置 Storm安装配置 整合Kafka+Storm 整合Storm+HDFS 整合Kafka+Storm+HDFS 参考 ...

  5. CSS样式覆盖顺序

    有时候在写CSS的过程中,某些限制总是不起作用,这就涉及了CSS样式覆盖的问题,如下 Css代码   #navigator { height: 100%; width: 200; position:  ...

  6. Android中使用反应式编程RxJava

    GitHut 地址: https://github.com/ReactiveX/RxAndroid (1)RxJava简介: RxJava 是一个在Java虚拟机上实现的响应式扩展库:提供了基于obs ...

  7. topcoder SRM 617 DIV2 SlimeXSlimonadeTycoon

    此题需要注意的两个地方是 (1)在某天生产出来的Slimonades,必须在stale_limit天内必须卖完,否则超过stale_limit内抛弃(东西都有保质期) (2)每天生产出来的Slimon ...

  8. Codeforce - Travelling Salesman

    After leaving Yemen, Bahosain now works as a salesman in Jordan. He spends most of his time travelli ...

  9. 【poj2828】Buy Tickets

    Description Railway tickets were difficult to buy around the Lunar New Year in China, so we must get ...

  10. 关于C# winform中使用pictureBox显示大红叉的原因

    pictureBox的关于image的属性有三 个,InitalImage,Image,ErrorImage分别表示picturebox的默认初始图片,当前可以设置的图片和出错之后默认显示的图 片,而 ...