腾讯Android自动化测试实战

腾讯Android自动化测试实战


腾讯Android自动化测试实战


丁如敏 盛娟 等著





图书在版编目(CIP)数据

腾讯Android自动化测试实战 / 丁如敏等著. —北京:机械工业出版社,2016.10

ISBN 978-7-111-54875-1

Ⅰ. 腾…   Ⅱ. 丁…   Ⅲ. 移动终端-应用程序–程序设计   Ⅳ. TN929.53

中国版本图书馆CIP数据核字(2016)第223713号




腾讯Android自动化测试实战

出版发行:机械工业出版社(北京市西城区百万庄大街22号 邮政编码:100037)

责任编辑:孙海亮 责任校对:董纪丽

印  刷: 版  次:2016年9月第1版第1次印刷

开  本:186mm×240mm 1/16 印  张:22

书  号:ISBN 978-7-111-54875-1 定  价:69.00元

凡购本书,如有缺页、倒页、脱页,由本社发行部调换

客服热线:(010)88379426 88361066 投稿热线:(010)88379604

购书热线:(010)68326294 88379649 68995259 读者信箱:hzit@hzbook.com


版权所有·侵权必究

封底无防伪标均为盗版

本书法律顾问:北京大成律师事务所 韩光/邹晓东





本 书 编 委

丁如敏

毕业于北京邮电大学,近10年的软件测试和项目管理经验,精通移动终端性能测试、自动化测试、敏捷测试等各种测试技术。在腾讯工作期间,带领团队共发明*专利50多项,开发10多门内部培训课程。喜欢挑战软件领域的各项前瞻技术,并有丰富的实践经验。

盛娟 

毕业于合肥工业大学计算机及应用专业,腾讯科技高级测试工程师。之前先后服务于中国联通、CISCO中国研发中心,有10多年的软件测试和项目管理经验。近两年主要负责搭建QQ浏览器Android端质量保证体系,积累了丰富的移动终端项目经验。

陈航特

毕业于南京理工大学电子信息工程专业,专注于Android端自动化测试,是国内较早进入该领域的探索者,有丰富的使用与二次开发Android原生自动化测试框架的实战经验。目前负责腾讯应用宝的客户端自动化测试与相应的平台搭建工作。

陈*

腾讯高级工程师,12年软件开发和测试工作经验,曾任职于多家知名跨国企业,现任职于腾讯公司,从事浏览器视频相关测试开发工作,并在视频领域取得多项*专利。擅长工具开发、前端性能测试和后台性能测试。

邓曦

毕业于电子科技大学,现任职于腾讯公司,担任无线研发部工程师,负责手机QQ浏览器(Android)测试工作。在测试领域拥有超过8年的经验,在自动化测试方面经验丰富。帮助手机QQ浏览器(Android)实现了从零到行业第一的飞跃。

高苡新

2006年北航本科毕业,10年软件测试经验、5年移动互联网测试经验,长期负责手机QQ浏览器性能测试,2013年至2015年专职负责网页打开速度测试。从零开始搭建手机QQ浏览器速度自动化测试方案。其所在的自动化团队获得2013年MIG移动互联网事业群SEVP特别奖。

林凯杰

腾讯专项技术测试工程师,主要从事ROM级别的App测试工作,在Android自动化方面有3年实践经验,对跨应用自动化实现有许多心得。自动化实现方式有很多,因需制宜最重要!

刘洋 

腾讯高级测试工程师,毕业于大连交通大学。加入腾讯前分别在华为、中国移动担任过系统测试工程师,主要从事网络、移动领域的工作。于2009年加入腾讯移动测试组,主要致力于精准、代码耦合、覆盖率、自动化等方面的研究与实施,擅长Linux、Android相关工作。

鲁万林

毕业于武汉大学,曾任职于华为公司,现任职于腾讯公司,担任无线研发部高级工程师。手机QQ浏览器Android平台测试负责人,在测试领域有10多年的经验,在成都从无到有建立了一只强大的浏览器测试团队,帮助QQ浏览器从零飞跃到行业第一。

万宇

2006年毕业于浙江大学计算机系。10年一流公司工作经验,先后在SAP、Oracle、腾讯等公司从事测试开发工作,目前负责Android手机QQ浏览器等产品的自动化测试开发工作。擅长工具开发,对性能测试、自动化测试有深入的理解。

郑若琳

2009年毕业于广东财经大学。加入腾讯4年,负责过QQ浏览器国际版、新蜂ROM、手机应用宝等移动端产品的业务测试和自动化测试,目前负责应用宝的测试体系建设和测试外包管理工作。具有多年测试实战经验,擅长自动化测试工具运用。

钟书成

毕业于成都信息工程大学和中国科学院,腾讯高级测试工程师。加入腾讯前曾在多个外企项目中从事测试开发工作,于2012年加入腾讯地图项目,主要致力于自动化测试的研究与实施,在Android自动化测试方面有丰富的经验。在进行腾讯地图项目期间还负责八爪鱼自动化测试平台的设计与开发工作。






序 Preface

最近和腾讯移动品质中心(TMQ)接触比较多,除了技术的交流,还邀请TMQ资深人士参加了某个软件工程论坛并做了分享,关注了TMQ公众号。现在很高兴为这个优秀团队的新书《腾讯Android自动化测试实战》写序,因为可以先睹为快,提前学习腾讯的经验。

现在移动应用很普及了,无须摆事实、讲道理,读者都深有体会。但10年前,移动应用还相对落后,那时TMQ就已经开始专注移动App的测试,故这个团队在移动应用专项测试、精准测试体系及自动化测试方面都有着丰富的实战经验。这本书就是他们2015年策划的移动测试领域的3本新书之一。这本书专注Android自动化测试,覆盖了从环境配置、UI元素获取、用例编写到脚本开发、编译、执行等整个移动应用的生命周期。针对常用的Android自动化测试框架和工具,如Appium、Monkey、Robotium和UIAutomator 等都进行了详细介绍,从其原理简析开始,循序渐进地介绍了其安装、设置以及API调用等知识,并围绕着实例详细介绍了其应用实践、技巧,读者一面看书、一面实践,就能轻松掌握Android自动化测试的技能。

虽然是小小的App应用,涉及的技术却不比桌面或Web低,反而由于资源更宝贵、网络连接不稳定、迭代更快、用户体验要求更高等,在单元测试、性能测试、压力测试、兼容性测试、速度测试等各方面都更具挑战性,测试人员还要面对Native、WebView和HTML5等不同技术。本书对上述所有内容,包括一些具体的技术细节,如非耦合式用例设计、API接口的封装等,都有很好的交代。书中还提供了完整的实例,从测试工程概览、签名开始,到测试用例编写、执行、管理,再到结合Spoon生成汇总报告,一气呵成。

注重品质的团队,写起书来也绝不会忽视质量,这本书就是一个典范。TMQ将书的质量放在首位,不仅选择最有经验的测试工程师组成一支很强的写作团队,而且初稿出来之后经过了6轮的内部评审,参加评审的人员之多、评审时间之长,是绝无仅有的,因此这样写出来的书,质量是有保证的。

本书不仅介绍了Android自动化框架的基础知识、原理和API使用,而且分析过程逻辑清楚,设计和实现思路清新自然,还触及一些较深的主题,如框架的二次开发等,故本书适合不同层次的测试人员和开发人员学习。借助网站的在线支持,本书如虎添翼,更加保证了读者的学习效果。

综上所述,本书是一本值得向大家推荐的好书,大家一定会喜欢的。有了“她”,轻松完成Android自动化测试也就不在话下了。


朱少民

于上海





前  言 Preface

为什么要写这本书

早在2010年年底,我们团队就有出一本关于移动互联网测试书籍的计划(那时候移动互联网测试书籍基本没有),当时计划的内容涉及面比较广,涵盖测试设计、测试用例管理、测试流程、自动化测试、专项测试等领域。不过,由于各种原因被搁浅,确实有点儿可惜,否则移动互联网测试国内的第一本书当时就面世了。这次终于又有机会整理这些年的测试经验并形成一本书了,借此可以跟业界的同行一起交流切磋。

TMQ(Tencent Mobile Quality)腾讯移动品质中心,是腾讯内部最早专注于移动App测试的团队,在10余年的时间内承担了近10款业界领先产品的测试工作,为腾讯向移动方向转型提供了多项质量方案和关键专利。本书的作者都是TMQ平台的核心成员,服务于公司级的手机QQ浏览器、应用宝等项目,经过这几年在移动测试领域的探索与实践,摸索出了一些实实在在的实践经验。TMQ的老板鼓励我们把这些知识和经验编写成册,这样不仅能为公司内部提供好的产品服务,也能为业界同人提供参考,从而将知识更好地扩散出去。我们团队非常珍惜这次写书的机会,故组织了团队内Android自动化测试方面经验丰富的同学一起来编写。大家都是利用自己的业余时间总结各自擅长领域的经验和知识,且初稿经过6轮的内部评审(参加评审的同事超过30位),前后历时半年多才最终完成了本书的写作和修改,希望能给读者提供一本质量较高的专业书籍。

TMQ经过这几年的积累,在专项测试、精准测试体系及自动化测试方面都有比较多的精辟之作。基于此,我们分小组对这些知识进行了整理,形成了相应的知识库,完成了本系列丛书,包括《移动互联网App性能评测调优实践》《精准化测试白皮书》《腾讯Android自动化测试实战》3本书,其他两本也在同期编写出版工作中。希望TMQ平台出品的书籍能给予读者思路上的指导或者是技术上的解惑。

除封面署名外,参加本书编写的作者还有:陈航特、陈*、邓曦、高苡新、林凯杰、刘洋、鲁万林、万宇、郑若琳、钟书成共12位作者(按姓氏拼音排序),都是来自腾讯移动端QQ浏览器及应用宝团队的骨干员工。

读者对象

本书是一本务实的书籍,案例都是作者们的第一手资料,对于软件质量保证方面的初学者,本书还提供了简短的案例以帮助其理解,循序渐进,掌握测试核心原理;对于有经验的同行,本书提供了经典案例帮助其提升与参考。这里根据行业实际需求给出了相应的用户群体:

对移动业务测试感兴趣的人;

对Android自动化测试感兴趣的人;

即将开展Android自动化测试的团队;

开设相关课程的院校师生。

本书特色

Android自动化测试经过这几年的发展,官方提供的开源自动化框架已经能非常好地支持终端的测试业务,但是如何利用、如何用好这些资源还是比较现实客观的问题,尤其是在小公司中,自动化测试方法的摸索及实施还存在一些困难,需要一定的投入才能得以真正应用。业界一些Android自动化测试方面的书籍很多都偏原理的介绍,而本书不仅深度解析这些框架的原理,还给出了手机QQ浏览器、应用宝项目中的典型案例,像最常见的App速度、要求较高的视频播放性能测试等,供需要实践的读者学习,这也是本书的重要特色之一。本书前半部分主要介绍业界流行的Android自动化框架的基础知识,聚焦工具框架的原理以及基础API使用、框架的二次开发改造(根据具体项目做相应修改),以及实践过程中一些共性问题的分享。如果读者已经掌握这些框架基础,那么对本书内容的理解就会更容易。同时读者可以重点关注本书中介绍的对框架进行二次开发的内容,并结合自己的实际项目考虑如何应用这些知识提升自己的工作效率;基础比较高的读者可跳过这部分直接阅读后半部分。后半部分通过一些实际案例来讲解自动化框架的应用,更强调系统性分析设计能力,包括需求的分析、工具选型、测试方案、代码覆盖率的应用等,覆盖功能测试、性能测试的具体实战案例。这部分对读者的技术能力要求相对更高一些,涉及的知识点的深度和广度要明显高于前半部分,需要进行Android App应用的性能速度测试的读者可以深入阅读,领会书中所提场景的测试设计与思路,进而掌握框架的精髓所在。在经典案例中也给出了很多具体实现思路的介绍与分析,让读者知其然、并知其所以然,同时各位作者也把项目测试工程代码加以整理,打包至TMQ后台,供读者下载,读者如有需要可以直接导入工程进行调试学习,以大大减少学习成本。读者可以根据自己的需求阅读相应章节的内容:如熟悉Java语言,又面临Debug未混淆被测App的情况,建议直接学习Robotium框架,因为Robotium操作简单、相关资料丰富,还能支持ant、maven打包,与jenkins结合较好;因Robotium不支持跨应用,所以对于需要支持跨应用的框架,读者可以阅读UIAutomator和Appium框架,其中Appium是借助WebDriver JSON协议实现的,能支持多种语言编写测试脚本;对于有一定经验的读者,在案例选择时可以结合Robotium和UIAutomator的优点一起使用,此时可直接阅读本书中的浏览器视频性能测试案例。

勘误和支持

本书作者分别在深圳、北京、成都、合肥四地办公,所以整个写作过程中异地沟通比较多,整书从选题至初稿形成,差不多花了5个月的时间,速度超过我们的预期,但由于作者的水平有限、异地交流、编写时间仓促等,书中难免会出现一些错误或者不准确的地方,恳请读者批评指正。为此,我们特意在TMQ的官网创建了一个在线支持:http://tmq.qq.com。你可以将书中的错误发布在新书勘误表页面中,我们将尽量在线上为读者提供最满意的解答。书中的全部源文件除可以从华章官方网站(http://www.hzbook.com)下载外,还可以从TMQ网站下载,我们也会将相应的错误及时更正。如果你有更多的宝贵意见,也欢迎发送邮件至邮箱dinahsheng@tencent.com,期待能够得到你们的真挚反馈。

致谢

感谢腾讯科技MIG无发研发部总经理冼文佟、助理总经理陈诚,正是他们鼓励我们多总结、多分享、把知识传播,我们才有了写书的想法。

感谢腾讯科技MIG无发研发部品质中心的总监廖志、李德广、张鼎,给我们提供TMQ这样好的一个人才培养平台,让我们不断成长和提升,同时也非常感谢他们在百忙之中指导我们写作。

感谢腾讯科技的同事们在整个写作过程中,帮我们多次进行内容及技术层面的审核指错,尤其是王琳同学,作为每章内容的第一位读者,给了很多好的建议和思路,其他同事也对本书的内容进行了细致的评审,他们是陆小三、吴景、柳炜、沈东雄等,在此一并谢过。

感谢腾讯科技的浏览器测试团队、应用宝测试团队,在我们编写过程中他们提供的工作支持,是我们得以这么快速完成的基础。

感谢同济大学朱少民教授给我们的专业指导和鼓励,让我们更有信心完成本书的编写。

感谢机械工业出版社华章公司的编辑杨福川、孙海亮,是他们多次给予我们指导及鼓励。

感谢我们的亲人在我们周末加班写作时给予的支持与理解,你们的支持是我们最大的动力!

谨以此书献给我们最亲爱的家人,以及热爱移动业务测试的朋友们!


丁如敏等





Contents 目  录

本书编委

前言

第1章 概述 1

1.1 Android自动化测试框架概述 1

1.2 本书内容概述 5

第2章 自动化测试框架及应用领域综述 8

2.1 自动化测试框架介绍 9

2.1.1 一个简单的Android App自动化测试过程 9

2.1.2 自动化测试框架基本原理 11

2.2 移动终端自动化测试应用场景 15

2.3 本章小结 17

第3章 Robotium框架工作原理及实践 18

3.1 Robotium常用功能 19

3.1.1 什么是Robotium 19

3.1.2 Robotium提供的类 20

3.1.3 环境搭建 21

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

3.2 Robotium原理简析 40

3.2.1 Robotium支持Native原理 40

3.2.2 Robotium支持WebView原理 46

3.3 Robotium实践运用 52

3.3.1 控件ID相同时获取控件 52

3.3.2 ListView列表遍历 53

3.3.3 修改Robotium以支持X5WebView 55

3.4 本章小结 58

第4章 Monkey基本原理及扩展应用 60

4.1 Monkey基础知识 61

4.1.1 Monkey概况 61

4.1.2 Monkey参数 61

4.1.3 Monkey事件 65

4.1.4 Monkey环境搭建 69

4.1.5 Monkey启动 70

4.2 Monkey测试方法 71

4.2.1 Monkey测试实例 71

4.2.2 Monkey日志分析 78

4.3 Monkey的基本原理 84

4.3.1 Monkey代码框架 85

4.3.2 Monkey代码逻辑详解 87

4.4 Monkey扩展应用示例 91

4.4.1 Monkey代码重编译执行方法 91

4.4.2 Monkey截图优化 96

4.4.3 Monkey Wi-Fi自动重连优化 102

4.4.4 Monkey扩展应用的优点和缺点 106

4.5 本章小结 106

第5章 UIAutomator框架及实践 107

5.1 UIAutomator简介 108

5.2 UIAutomator解读 109

5.2.1 UIAutomator框架解读 109

5.2.2 UIAutomator原理解读 114

5.2.3 UIAutomator API解读 120

5.3 UIAutomator实战 131

5.3.1 UIAutomator快速上手 132

5.3.2 UIAutomator设计思想 138

5.3.3 UIAutomator实践案例 141

5.4 UIAutomator总结 151

5.4.1 UIAutomator代码规范及建议 151

5.4.2 UIAutomator技巧及封装 152

5.5 本章小结 155

第6章 Appium框架解析及实践 156

6.1 Appium框架概况 157

6.1.1 Appium架构原理 157

6.1.2 Appium框架的优缺点 158

6.2 Appium框架工作解析 159

6.2.1 Appium环境搭建 159

6.2.2 HelloWorld测试示例 163

6.2.3 Desired Capabilities的说明 168

6.2.4 Appium API的解读 171

6.3 Appium框架在腾讯地图中的实践 175

6.3.1 Appium接口的封装 175

6.3.2 测试脚本设计思想 181

6.3.3 Appium在腾讯地图中的测试实践 182

6.3.4 Hybrid App的测试方法 189

6.3.5 Appium脚本常见问题及处理方法 191

6.4 本章小结 193

第7章 Android App速度测试 194

7.1 速度测试场景 195

7.2 速度测试的六大方法 196

7.2.1 掐表计时法 198

7.2.2 打印日志计时法 199

7.2.3 图像分析计时法 200

7.2.4 Hook方案计时法 203

7.2.5 网络包分析法 207

7.2.6 各种速度测试方法的优缺点 209

7.3 手机QQ浏览器网页打开速度测试实践案例 209

7.3.1 确定关键指标 209

7.3.2 选择测试方法 210

7.3.3 整体方案 211

7.3.4 解决关键问题 212

7.3.5 速度优化效果 228

7.4 手机QQ浏览器多窗口按钮速度实践案例 228

7.4.1 为什么要做多窗口按钮速度测试 229

7.4.2 什么是多窗口按钮速度测试 229

7.4.3 多窗口按钮速度测试影响因素和测试方法 230

7.4.4 如何进行多窗口按钮速度测试 231

7.5 本章小结 234

第8章 视频性能测试案例 235

8.1 视频性能测试需求分析 236

8.2 视频首帧性能测试方案的设计思路 237

8.2.1 视频播放流程 237

8.2.2 设计思路 238

8.3 视频首帧性能测试方案的具体实现 240

8.3.1 开发工具准备 240

8.3.2 测试环境准备 241

8.3.3 工程部署 242

8.3.4 关键代码和难点分析 244

8.3.5 编译环境配置 262

8.3.6 工具安装 264

8.4 方案优缺点 265

8.5 本章小结 265

第9章 应用宝BVT测试案例 267

9.1 测试工程 268

9.1.1 测试工程概览 268

9.2.1 测试工程签名 269

9.2 测试用例 271

9.2.1 测试用例生命周期 271

9.2.2 测试用例编写 273

9.2.3 测试用例执行 277

9.2.4 测试用例管理 279

9.3 测试报告 279

9.3.1 Spoon介绍 279

9.3.2 结合Spoon的出错重试与截图 282

9.3.3 结合Spoon生成汇总报告 287

9.4 Robotium跨应用 287

9.4.1 UIAutomator Dump方式跨应用 288

9.4.2 UIAutomator结合Instrumentation模式 289

9.5 代码覆盖率 292

9.5.1 覆盖率定义 292

9.5.2 覆盖率工具 293

9.5.3 JaCoCo介绍与实践 295

9.5.4 BVT测试与覆盖率结合 305

9.5.5 指导建议 309

9.6 本章小结 309

第10章 兼容性测试实践 311

10.1 兼容性测试概述 311

10.2 兼容性测试方法 313

10.2.1 手动测试 313

10.2.2 自动化测试 314

10.2.3 云平台测试 324

10.3 兼容性测试思考 336

10.4 本章小结 336

第1章

概  述

在展开各章节简介之前,本章先带读者了解一下Android自动化测试框架的大体历史以及框架的演进过程。Android自动化测试框架和工具从2009年发展至今日趋成熟,从早期官方提供的半自动化演进到全自动化框架,包括支持跨应用、WebView等,其功能越来越强大,并融合库思想、数据驱动、模块化、函数桩等先进的自动化测试思想和理念,Android测试起来越便捷。本章主要介绍Android App自动化框架的历史及热点问题。

1.1 Android自动化测试框架概述

2007年Android开源时,Monkey、Instrumentation和MonkeyRunner这3个测试框架,是跟Android源码一起发布的,这也是最早可用的自动化测试框架,那几年大家基本都是用这些框架来开展自动化相关测试工作的。2010年,第一个第三方的测试工具Robotium(基于Instrumentation)发布了,不少测试人员就转用这个框架,Robotium社区逐步发展起来。图1-1所示为Robotium热度随时间变化的趋势。

2010年还有一个自动化测试框架Robolectric开源了,主要支持单元测试;Robolectric 允许用户做大部分真实设备上可以做的事情,且可以在常规的JVM持续集成环境中运行,不需要通过模拟器,因此可以摆脱模拟器启动慢的问题。



图1-1 Robotium热度随时间变化的趋势

注:引用自https://www.google.com/trends/explore#q=robotium

2011年新发布的Android SDK包含了chimpchat,可以通过Java来调用Monkey,也可以用Java语言实现类似MonkeyRunner功能(MonkeyRunner之前只支持Python)。

2013年则是一个Android自动化测试框架爆发年,Selendroid、Espresso、Calabash、Appium等框架都是在这一年发布或者开源的。其中,Appium整合Selendroid以及UI Automator等框架的优点,使用Selenium的WebDriver JSON协议,使WebDriver用户使用起来非常方便。自2013年以来,Appium发展非常迅速(不过基本上是印度的同学在搜索)。图1-2所示为Appium热度随时间变化的趋势。


图1-2 Appium热度随时间变化的趋势

注:引用自https://www.google.com/trends/explore#q=Appium

按照时间线,把上面介绍的自动化测试框架梳理一下,如图1-3所示。

当然国内也有不少团队或者个人开发自动化测试框架并开源,例如百度的Café(https://github.com/BaiduQA/Cafe),阿里巴巴的两个自动化测试框架Athrun)http://code.taobao.org/svn/athrun)、Macaca(http://macacajs.github.io/macaca/)。腾讯内部也开发了Android自动化框架,不过暂时没有开源。相信国内其他公司也在开发相关自动化框架,有些公司基于已有的开源框架二次开发定制适合自己项目的自动化框架,可能暂时也没有开源。


图1-3 Android自动化测试框架时间线

注:上面提到的是相对有一定使用人数的自动化测试框架,除此之外,业界还有不少其他测试框架,如MonkeyTalk、RoboSpock、NativeDriver等。

上面简单介绍了Android自动化测试框架的历史(时间线展示)。Android自动化测试里面还涉及到一个签名的问题(Instrumentation的限制),我们按照重签名的维度重新划分一下,方便读者做自动化测试框架的选型。图1-4所示为Android自动化测试框架图谱,仅供参考。



图1-4 Android自动化测试框架图谱

注:Appium不需要更改被测App签名;

Hybrid:混合Android原生控件以及Webview控件;

Native:纯Android原生控件。

1.2 本书内容概述

本书主要介绍Android自动化测试相关内容,分为以下两大部分。

第一部分介绍业界流行的Android自动化框架的基础知识,聚焦工具框架的原理和基础API使用,以及框架的二次开发(根据具体项目做相应修改),分享实践过程中的一些共性问题。这部分内容需要读者有基本编程能力(主要是Java方面的编程基础),如果读者已经具有这方面的能力(比如独立按Android开发者官网文档搭建相关HelloWorld的工程),那就更容易理解书本的内容;同时可以重点关注如何进行二次开发,以及如何应用于实际项目。

第二部分通过一些实际案例来讲解这些自动化框架的应用,更强调系统性分析设计,包括需求的分析、工具选型、代码覆盖率的应用,以及覆盖功能测试以及性能测试的具体实战等。这部分对读者的技术能力要求相对更高一些,涉及的知识点会多一些。当然如果有不太明白的地方,欢迎与各章节的作者进行交流。

第一部分Android自动化测试基础篇。

本书第一部分内容就是从自动化框架图谱中选择业界相对流行的自动化测试框架展开介绍,根据Google上这些自动化测试框架搜索量以及我们了解到的各互联网公司的自动化框架使用情况,我们选择有代表性的4个框架(Monkey、Robtotium、UI Automator)Appium)分别进行介绍。第1章也即本章,所以从第2章开始简介。

第2章自动化测试框架及应用领域综述:本章通过一个浅显易懂的Android自动化测试案例进行详细讲解,提炼出通用的自动化测试框架的原型—“动作执行/结果判断/报告展示”,最后介绍自动化测试的各种应用场景,方便读者更好地应用自动化测试。

第3章Robotium框架工作原理及实践:本章要求读者有一定的Robotium基础,可以先到Robotium官网下载相关Robotium的基础Demo练习。第一节先简单介绍Robotium的特点以及优缺点,再到Robotium主要API的详解,对Native控件以及Webview控件分别从获取以及相关操作展开介绍,同时还有不少实践过程中技巧的分享,例如搜索以及等待时间、截图、断言等。第二节深入Robotium的代码框架,结合代码分析,分别介绍控件获取以及操作的具体实现原理;针对最近又流行起来的H5页面,着重介绍Webview的基本原理,方便读者对H5页面进行测试。第三节通过分享3个实际案例(都是测试人员会经常碰到的案例),方便读者处理类似问题。第一个案例是解决相同ID或者没有ID的控件;第二个案例是对ListView在屏幕之外操作技巧;第三个案例则是针对一些定制化的Webview(例如腾讯浏览器服务X5Webview)进行适配,让Robotium也能快速支持定制化Webview的自动化测试。

第4章Monkey基本原理及扩展应用:每个从事过Android App测试的同学都应该使用过Monkey工具。Monkey工具也是最基础的自动化工具之一,上手比较容易,因此基本是大家的首选。本章第一节从Monkey基础知识开始介绍,从参数配置到环境搭建做基本解读,让读者能够通过本小节启动自己的Monkey自动化测试。第二节通过测试实例讲解Monkey具体使用,以及使用Monkey发现crash后产生日志的统计分析,解决实际测试过程中的一些问题,让读者能结合自己的项目做Monkey自动化测试。第三节通过Monkey的源码来介绍Monkey的代码基本框架以及某些逻辑详解,让读者清楚地了解Monkey运行逻辑(需要用户有相关Java代码基础),使得读者“知其然更知其所以然”。Monkey提供的功能可能没法满足要求,我们需要通过对Monkey的二次开发来定制需求,第四节通过两个实际案例来详细介绍Monkey的二次开发过程,这样就方便读者动手二次开发自己的需求。

第5章UI Automator框架及实践:本章第一节先简单介绍UI Automator的发展历程以及特点,让读者有一个基本认识。第二节介绍UI Automator整体框架、UI Automator各个类以及它们之间的关系,然后重点介绍五大基础类UiDevice、UiSelector、UiObject、UiCollection、UiScrollable。接下来重点介绍该框架两个重要事情—界面解析和事件注入,通过代码解读这两块的基本原理。最后把API都注解一下,方便读者查询。第三节主要通过实战案例来展示UI Automator的使用,从功能测试到性能测试以及压力测试都有相关案例讲解,基本测试类型自动化都可以选择UI Automator来完成;同时总结使用过程中的问题,如输入法、第三方包编译、adb稳定性等问题,方便读者借鉴思路。

第6章Appium框架解析及实践:本章第一节介绍Appium基本架构原理以及使用到的一些技术点,同时说明Appium框架的优缺点,让读者有一个大概认识,方便做自动化测试框架选择。第二节从环境搭建入手,手把手教读者完成一个HelloWorld的测试示例,接下来针对日常可能用到的方法对API进行解读,让读者逐步上手。第三节开始介绍自动化测试一些进阶思路,例如接口封装以及用例设计思想,引导读者把自动化测试做得更好。接下来以腾讯地图自动化测试实践为案例,分别从可重复性、稳定性和可维护性三个方面详细介绍,同时对Hybrid App测试做了介绍,最后对Appium实践过程中常见问题做了解答,方便读者借鉴,避免走弯路。

第二部分Android自动化测试实战篇

第7章Android App速度测试:本章选择对用户体验感知明显的一个性能点—App速度,针对App速度测试来进行深度分析,从整个需求的系统分析,再到详细对比不同速度测试方法(掐秒表、打Log、录像、Hook方式、网络包分析等),最后提炼出速度测试的相关方法论,以供读者参考。

第8章视频性能测试案例:本章选择视频性能这个需求进行系统性分析,从用户感知层面出发,挖掘相关需求点,再到整体性能方案的设计(从自动化执行以及结果对比等都做详尽的分析),最后到结果分析等方面,都做了系统性详细介绍,给读者一个完整的案例。这一章对读者基础能力要求有点儿高,除了涉及Java编程语言,还涉及C的编程(JNI),同时要求在视频方面有一定的基础(如FFMPEG)。

第9章应用宝BVT测试方案:Robotium在应用宝项目的实践案例。主要介绍如何利用Robotium来做BVT(Build Verification Test), 把App最基础功能梳理出来,然后写成自动化测试用例,每次持续集成编译成功,都会运行这些自动化脚本,确保每个版本基础功能可用。同时结合代码覆盖率,可以关注覆盖度情况。代码覆盖率主要用JaCoCo生成,当然Emma也使用很广泛。

第10章兼容性测试实践:Android手机从2007年发布,到目前为止,有超过2000种型号,系统从2.X到6.X,Android碎片化比较严重,那么怎么保证App在大多数的机型系统中正常运行呢?这一章介绍如何利用业界的云测试系统进行测试。云测试系统包括百度MTC、Testin、优测,通过使用这些云测试系统可以快速发现一些兼容性问题。







第2章

自动化测试框架及应用领域综述

近几年,随着移动互联网的快速发展,智能终端的App应用越来越广,Android测试技术也备受重视,新的终端自动化测试框架层出不穷,本章笔者就自动化测试的入门知识及其应用做一个浅显的梳理与总结,与读者一同探讨移动终端自动化测试思路和方案。同时,本书主要也是围绕本章节提到的基础框架及其应用场景进行实战分析与演练,以亲身体验总结出实际项目经验,给准备实施或正在实施自动化测试的读者提供一些帮助和建议。

自动化测试在软件测试的各大沙龙、行业峰会以及培训课程中都是一个热门的话题,很多测试人员也非常热衷于自动化前瞻知识的研究和学习。那么Android自动化测试框架如何理解和定义呢?每个人给出的答案不一定完全一样,笔者认为,狭义上就是通常说起的自动化测试框架,像很早就风靡的Robotium、后起之秀UI Automator、跨平台的Appium以及类似于Monkey的稳定性工具等,广义上包括自动化测试框架和测试管理平台;前者通用于狭义概念上的理解,后者主要是测试中对测试用例、执行、资源调度、问题提单、数据统一存储、报告输出等进行综合的展示平台。本书主要介绍基于狭义定义的自动化实践,本章知识结构图如图2-1所示。





图2-1 本章知识结构图

2.1 自动化测试框架介绍

2.1.1 一个简单的Android App自动化测试过程

在了解相关的Android App的自动化测试框架之前,先来看一个常用的自动化测试实例,这里先不讨论框架,主要是测试用户操作的模拟、执行结果的判断,以便获得对测试自动化的感性认识。

案例需求如下:QQ浏览器打开手机存储卡的文件,通过自动化测试获取其打开某一文件的响应时间,这里首先需要做细分,把需求拆分为几个关键点,即进入浏览器、文件打开操作、获取手机屏幕、截图分析、结果统计输出。自动化测试就是实现机器完成这些关键点的一系列操作,并且在脚本的实际运行中添加需要的业务逻辑判断,实现测试自动化。根据脚本的具体实现,整理出打开文件测试流程图,如图2-2所示。

1. 准备测试工具

下载record和replay的脚本录制工具,有很多工具可以实现,比如本书中的MonkeyRunner、UIAutomator等,在这个案例里从网上下载record /replay可执行工具,直接复制至手机硬盘。

2. 录制测试脚本

在PC上连接手机,打开adb shell命令,进入record工具存放目录,用shell命令运行record工具启动用户脚本录制,并给录制命名,如wps(打开wps文件)。工具运行成功之后,手工完成所需要的业务操作(如手机主页→选择QQ浏览器→文件目录→双击wps文件),操作结束后,按Ctrl+C键结束脚本的录制工作。录制脚本过程如图2-3所示。


图2-3 录制脚本过程

3. 执行测试脚本

这里以Java语言进行测试脚本的编写作为示范,通过Java的ProcessBuilder类在PC上创造并启动一个cmd命令行的进程,并执行“adb shell replay”操作进行脚本回放,这样打开文件的功能就可以通过脚本完成相应的操作执行,如代码清单2-1所示。

代码清单2-1 实现浏览器中打开wps文件

public static synchronized void enterWPS()

{

ProcessBuilder pb = new ProcessBuilder("cmd.exe");//ProcessBuilder("/system/bin/sh");

// java.lang.ProcessBuilder: Creates operating system processes.

pb.directory(new File("C:\\"));

// 设置shell的当前目录。

try {

    Process proc = pb.start();

    BufferedReader in = new BufferedReader(new InputStreamReader(proc.getInputStream()));

    // 获取输入流,可以通过它获取SHELL的输出。

    BufferedReader err = new BufferedReader(new InputStreamReader(proc.getErrorStream()));

    PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter (proc.getOutputStream())), true);

    // 获取输出流,可以通过它向SHELL发送命令。

    out.println("adb shell replay enter wps"); 

    //打开WPS文件

    out.println("exit");

    String line;

    while ((line = in.readLine()) != null)

    {

        System.out.println(line);

    }

    while ((line = err.readLine()) != null)

    {

        System.out.println(line); 

    }

    in.close();

    out.close();

    proc.destroy();

    }

catch (Exception e)

{

    System.out.println("exception:" + e);

}

}

}

这里的录制脚本存放路径需要读者在实践时进行更改,或者把录制的脚本放置在/system/bin/。

4. 结果判断

这里通过测试脚本完成了用户操作的模拟实现,但是正常的测试还需要结果的验证,需要编码脚本进行测试结果的判断,本案例可以通过截屏和图片分析wps文件是否打开成功,可以通过Google汉明距离相似度对比算法得到图片相似度来判定打开的结果,代码相对要复杂一些,后面的案例中也有类似的代码实现,这里就不再细述。

2.1.2 自动化测试框架基本原理

经过前面的一个简单的自动化测试案例,我们对Android的自动化测试有了一个感性的认识,很多有相关工作经验的测试同学也都会理解,这和PC的自动化测试思路是相通的,只不过所借助的框架不同,目前业界已经有很多成熟的开源Android端自动化测试框架,经常用到的框架代表有Robotium和UI Automator,各个框架可能在具体应用上有些不同,如有些偏稳定性,有些适用于Web应用,有些能支持跨应用,等等,但其主要思想是通过控件的位置、名称、属性等获取控件对象,并且对控件对象或者坐标模拟用户操作,测试同学通过这些控件的操作和状态变化来完成自动化测试的执行。图2-4里罗列了目前常用的测试框架,也是本书中实践的重点,后面的章节会详细讨论这些框架的原理及应用。



图2-4 目前常用的测试框架

这一小节讨论的自动化测试框架,是在实际项目中总结出来的且基本能运行的通用基础框架原型,它包括三个核心部分:一是如何获取坐标/控件并操作控件模拟用户端事件,二是脚本中的结果如何判断,三是测试结果报告的输出与展示。不管测试人员使用的是Robotium还是其他框架,万变不离其宗,通用原理都可直接图解为图2-5所示的这几个模块。


图2-5 Android自动化测试基本框架模块

1. 动作执行

自动化测试的首要条件是能够操作控件,最好像开发同学一样操作控件,如何实现呢?一种最常见的脚本录制方法,其主要思想是记录控件的坐标位置和发生的事件,通过回放脚本完成测试事件流,像Monkey Runner框架就提供比较方便的录制回放功能;另一种方法就是通过工具(比如:源码、UIAutomatorviewer等)获得测试界面的控件布局,找到目标空间的ID、名字、描述或者位置信息。测试框架可以通过这些信息得到控件对象,并对控件对象执行一系列事件操作像Robotium、UIAutomater等,这个阶段理解为测试的动作执行。

当然对于有跨应用App的控件操作会受到Android进程安全限制,这对于跨应用的操作是一个难点,举个简单的跨应用例子,在测试一款App应用时,它的某个功能会调起系统相机拍照,那这个功能就会涉及跨应用了。像Robotium就无法调用系统的一些INPUT事件完成跨应用的控件操作(其实Robotium从Android 4.3之后开始支持UIAutomation框架,理应可以支持跨应用的),基于Robotium框架的测试脚本跟被测对象需在同一个App或者可以相互访问,一般要求重新签名打包。所以在选定框架时就需要考虑相关的权限问题,当前可以直接支持跨应用的框架有MonkeyRunner、UIAutomater等, 后面的章节会详细给出主流框架的分析和使用建议,这里不再细述。

2. 结果判断

自动化测试中非常重要的一个环节就是测试步骤的验证,如何能不进行人工干涉而去正确并且自动地检查验证点,这是自动化测试环节中的一个关键点,有些可以借助测试框架的截图对比(像MonkeyRunner)直接完成结果输出,但在实际项目中会有很多具体业务,验证点会比较困难,不同的案例可以用拆解和辅助的验证方式来完成。比如播放器播放视频时,如何验证视频播放成功,这里就需要设定很多验证点。若播放的时间≥0,可以直接获取video标签里的current time来判断;若是功能测试,则有两种情况:①播放时有画面,可以直接截屏获取;②播放时有声音,可以获取声卡捕捉声音的数据,分析波形文件,但实现起来难度就比较大,后面的章节中有具体的案例介绍。基本结果验证的方法概括如下。

(1)截图对比。对于GUI的测试,一般会采用截图方式,但最简单的截图也需要考虑到很多附带的条件,大部分框架会有此功能(没有的话也不要紧,可以直接用系统自带的screencap或者其他工具配合使用),但它每秒能截取多少张图片?能否满足我们需要的图片判断条件?截取的图片如何做对比,对比的参照物是什么?做性能测试时,我们的工具是否会影响性能?要不要选择拍照工具而不影响性能?获取图片的速度能否满足性能测试要求?对比的参照物又如何设定?这些信息在每个具体的案例中又会衍生一系列新的问题。就一个具体的案例来说,浏览器打开网页的首字响应时间测试中,对比参照物可以是一张空白的图片,把指定的像素区域与这张空白图片做对比就能判定结果;而浏览器播放视频时的响应时间测试时所截取的图片,就不能这样判断,因为很多视频会在播放之前就显示一个关键帧的图片。为什么会这样?本书后面的章节会针对一些典型业务进行由浅入深的案例分析与工具设计,会用到基本框架但不局限于此,还需要测试人员更多的对工具和框架的配合应用与改造能力,完成系统性的业务测试需求。

(2)控件对比。Android UI自动化测试最直接面对的是应用程序界面,界面上存在按钮(button)、文本框(textView)等,我们都称之为控件。有些应用程序的执行逻辑直接体现在控件的状态显示上,如按钮状态的变化、文本的改变等。常用的自动化测试工具Robotium和UI Automator都提供了获取应用控件的接口, 当执行完一定的自动化测试逻辑后,可以将获取控件上的信息与预期的信息进行对比,判断测试结果是否通过。比如测试一个计算器程序,执行3乘以5的测试用例后,可以在结果输出框得到输出的值(result),用框架的结果与预期的结果做对比:assertEqual(result, 15),判断这个测试用例的结果是否通过。

(3)日志分析。日志分析可以作为一个辅助结果判断,比较适合在集成测试阶段做的一些接口、稳定性和性能测试,因为这个阶段产品的日志一般处于打开状态,测试同学可以很方便地收集到日志关键字信息。比如在进行稳定性测试时,可以直接把所有日志输出至文件,在测试执行程序中加入简单的文本处理,对日志信息进行分类检索,像在Java层出现 crash时,可以搜索日志中的FATA EXCEPTION/ANR等关键字,提取后面的几十行日志信息进行筛选处理。日志使用的更高进阶是日志埋点,对需要测试的接口埋入上报信息,一旦测试执行(用户使用)到该点,启动上报接口(或者SDK)送至指定的路径,就可以直接准确地看到测试结果,一般在灰度发布后这些日志信息会被关闭,总体上不会影响产品的功能。但日志的使用也有一些局限性,比如测试同学对日志tag不熟悉,需要先了解代码信息等,还有竞品对比测试时因为拿到的竞品包都是混淆的正式发布包,没有关键日志信息输出,可能需要用其他的手段来判断结果,比如控件对比(当然高手例外,他们可能会使用逆向工程,反编译再注入需要的log)。

3. 报告展示

报告展示一般是指给出整个测试的结果信息汇总并进行简单的分析,测试结束后直接输出预警和初步的数据报告,以邮件或者其他形式直接周知项目参与人员。如果执行较大的项目测试,就可能需要多维度、多指标地对待测应用的测试数据进行整合,要求测试同学对平台有较高的设计能力,比如平台需要实现产品中核心业务的性能benchmark测试结果及分析,BVT(Build Verify Testing)测试中发现的问题预警,功能自动化测试脚本中的BUG自动提单,等等。有些自动化测试框架里也带有简单的测试管理平台,根据业务类型决定这些平台是否能满足需求,有的产品需要平台支持多业务的横向与纵向对比信息、竞品的评分信息等,这时就需要做二次开发,形成一个系统性的测试管理工具,也即前文中说的测试的管理平台框架。

2.2 移动终端自动化测试应用场景

移动应用的特点是快速迭代、快速发布,基于移动应用的测试就逐渐转变为轻量测试、灰度发布机制、众测、后台云控系统等,甚至有损发布,很多类似的产品不断涌现,随着市场和用户日趋成熟,产品的功能差异性不大,那么竞争的关键就转移至品质与口碑,就要求研发团队严把质量关卡,赢得用户口碑。在这样的前提下,需要在质量团队引入更强大的平台或者用测试方法支撑业务的品质,要求自动化测试的思路深入至品质的各个角落,以真正为产品质量服务。

不妨梳理一下在应用App测试的时候,需要在哪些耗时耗力以及特殊要求的场景进行自动化测试。读者首先可能会想到兼容性测试,因为众所周知的Android碎片化问题,不同的机型适配问题可能五花八门,这里就需要有合适的兼容性方案去尽可能地覆盖测试,在实际项目中也确实是这样。这里,和读者一起来梳理一下移动平台自动化测试到底能做什么,哪些场景更适合用自动化测试,这些自动化测试是否跨越了我们传统的误区。图2-6所示为笔者梳理的Android自动化测试应用场景,每个领域里内容不是非常齐全,目的是引导读者一起去思考如何完善质量保证体系的自动化测试场景。


图2-6 Android自动化测试应用场景

(1)性能测试。移动终端应用,不管是Native还是WebView的应用,对性能要求都非常高,主要是卡顿、耗电、速度这几个常见关键性的指标,而这类测试重复性强,指标路径固定,并且质量指标中又需要分为横向与纵向对比情景,等等,形成一个庞大的测试矩阵,自动化测试支持才能更快捷地完成测试任务,一般性能测试会考虑选用自动化方案,此方案非常适合性能测试。

(2)稳定性测试。Android平台一般都会联想到用系统自带的Monkey工具进行测试,此工具既易上手也实用,但运用起来有非常多的讲究和技巧,简单的Monkey工具不一定能完成使命,在测试中也需要花费心思去对原生的Monkey进行改造,以满足不同业务的稳定性测试需求。

(3)功能测试。关于功能测试的争议比较多,因为产品都需要快速迭代,而脚本的稳定性、实现时间等成本开销大,真正发挥作用也需要不断地打磨,并且还有很多后期维护成本,所以比较折中的办法是做一些BVT测试和持续集成配合,在开发编译新的build后直接运行这些核心的BVT用例,以免出现严重的Regression/Block问题,日常的工作中选定较小范围的用例及适合的框架一般就可以解决问题。

(4)兼容性测试。不同的业务可能会有不同的适配要求,现在比较常用的方法是直接使用业界比较成熟的测试平台,如Testin、百度MTC、腾讯优测平台等,一般情况下平台能提供几百甚至上千台机器进行测试。本书后面也给出了兼容性测试方案,供各读者在项目中实践。

(5)接口测试。这块的测试主要是集中一些重要的API测试,和PC端的接口测试思路一样,都是通过脚本去遍历所有重要的参数等,并且抛开界面的干扰快速测试以至稳定。像浏览器里常见的就有JS API接口测试,当然这块可能需要开发同学的接口定义文档或者口头支援,梳理业务的关键API和参数列表以及相应的依赖关系等,是非常适合用自动化测试去实现的,脚本也相对简单稳定,而且效果明显。

(6)单元测试。Android终端用Android Junit可以快速方便地实现单元测试。很多公司单元测试工作都是由开发同学自行完成,但在移动互联网时代,基于敏捷开发测试前移的大环境,部分测试同学也会直接参与单元的编写和执行,比如,腾讯Tencent OS(TOS)项目团队就是由测试同学进行单元低层OS系统的单元测试,后面的章节也会讲到这块的测试案例。

(7)线上监控测试。这块测试方向不应该直接归属于传统的自动化测试范畴,因为它不需要常规情况下提到的自动化测试框架支持,也不需要开发测试用例脚本,这里主要是对线上测试数据的监控,并且利用大数据分析进行“自动化”测试,在互联网产品中极为适用而且能非常直接地体现产品的质量。举个简单的例子,通过浏览器的网页浏览功能,可以监控用户在浏览网页时有多少个浏览失败的网站、是否会出现必然浏览失败的网站、出现浏览失败的网站的地域/DNS是什么等,如此层层过滤,最后得到的关键信息会直接指导测试人员缩小测试范围,提高测试效率。但本书中没有准备这方面的案例,笔者在梳理这块内容时,为了保证知识的完整性在这里做一个小的铺垫,本书再版时会添加这方面的案例。

2.3 本章小结

其实自动化测试只是日常测试中的一个辅助手段,说白了,它就是利用自动化测试框架以及工具能理解的程序代替人去完成测试,通过比较执行的结果来判断测试是否通过。它的优势很明显,无论多复杂的操作,即使反复执行成千上万遍,只要程序没有问题,它都会快速地执行完毕,而且比人工轻松得多;但劣势也非常明显,最大的缺点就是工具是人工思考之后的一个固化程序,它不会变通,是按照人的旨意来完成相应的任务。所以测试人员对工具的应用和改造能力显得尤为重要,通过对被测应用的深入认识与思考,转化成可以实现的测试需求,再配合这些工具和擅长代码的同事进行封装,可以完成测试中各个领域的人工替代工作。本章对移动终端App应用的开发、测试和上线各个阶段需要的测试进行自动化测试梳理,分析自动化测试的应用场景,为后面的框架内容和案例提前做铺垫。






第3章

Robotium框架工作原理及实践

2010年,当Android还处于发展早期时,拥有丰富自动化测试经验的Renas Reda创建了Robotium项目,在Robotium发展到4.0版本时开始支持App中的Web自动化,经过几年的发展,Robotium现在已经是一款成熟、全面、稳定的自动化测试框架。更重要的是,Robotium是一款开源的测试框架,在世界各地都有活跃的贡献者对其进行更新与维护,因此,无须担心将来Robotium会随着Android的发展而变得不可用、不易用,相反,Robotium每天都在变得更加强大。

任何技术都离不开基础知识。首先,本章将介绍Robotium是什么以及有关Robotium的一些基础知识,让读者了解Robotium的基本规则。其次,将从Native和WebView两方面简析Robotium测试框架的运作原理,并介绍Robotium的实际应用以及笔者在实践过程的一些经验技巧,以加深读者对Robotium测试框架的理解。最后,本章选取一般项目中常见的一些场景介绍如何使用Robotium解决实践中的问题。

本章知识结构图如图3-1所示。

阅读完本章后,读者应该能比较全面地了解Robotium测试框架并知道如何使用了,由于本章只介绍Robotium相关知识,关于Robotium在项目方面的实际应用则可阅读第10章。




图3-1 本章知识结构图

3.1 Robotium常用功能

3.1.1 什么是Robotium

Robotium是一款类似Selenium但面向Android端的开源自动化测试框架,既支持测试Native应用,也支持测试Hybrid应用(混合模式应用,指介于WebApp与NativeApp两者之间的App,兼具Native App良好的用户交互体验的优势以及Web App跨平台、易变更的优势);既支持黑盒形式的自动化测试,也支持白盒形式的自动化测试。通过Robotium用户可以编写出更强大健壮的UI自动化测试用例,并可以应用在功能测试、系统测试、用户验收测试等多种测试场景中。Robotium主要具有以下优势:

同时支持Native应用和Hybrid应用。

由于是基于Instrumentation的测试,测试代码运行于被测应用所在的进程,控件识别与模拟UI事件都可以快速执行,因此测试用例执行速度更快。

由于是通过在运行时识别控件而非通过固定坐标方式,因此测试用例可以更健壮。

由于支持黑盒方式,不需要深入了解被测应用即可开展测试,因此编写用例花费的时间可以更少。

由于可以通过Maven、Gradle或者Ant运行测试用例,因此可以很好地作为持续集成的一部分。

Robotium缺点:

由于是基于Instrumentation的事件发送,因此无法跨应用。

代码运行在被测进程,可能影响被测进程的内存、CPU占用,若用于性能监控数据会有误差。

注:项目开源地址:https://github.com/RobotiumTech/robotium

3.1.2 Robotium提供的类

Robotium对外主要提供以下几个类:

By:Web元素的选择器。

Condition:接口类,用于等待。

RobotiumUtils:工具类。

Solo:对外提供各种API。

Solo.Config:Solo配置类。

SystemUtils:系统级工具类。

TimeOut:Solo配置类。

WebElement:Web元素的抽象类。

其中Solo类是主要对外提供各种API的类,Solo类采用中介者模式,持有com.robotium.solo包下的其他类的实例对象,当我们调用Solo类中的API时,大多数是转而调用com.robotium.solo包下其他类的方法。com.robotium.solo包下主要有以下类:

Getter:提供控件获取相关API。

ActivityUtils:提供Activity相关API。

Asserter:提供断言相关的API。

Clicker:提供模拟点击相关的API。

ScreenshotTaker:提供截图相关的API。

Scroller:提供滚动相关的API。

Searcher:提供控件搜索相关的API。

ViewFetcher:提供控件过滤相关的API。

Waiter:提供控件等待相关的API。

WebUtils:提供Web支持相关的API。

Robotium为了简化测试用例的编写,将以上的这些类都置为protected,对外只提供Solo类,因此,在编写测试用例时,主要实例化Solo类即可,本章介绍的API默认也均为Solo类中的方法。

3.1.3 环境搭建

使用Robotium进行自动化测试的工程采用的是Android Junit Test工程,基础环境与Android开发环境一致,为了方便本书第3章及第10章的讲解,本书的官网(http://tmq.qq.com/)附上了改造后的NotePad和NotePadTest工程,环境搭建步骤如下:

步骤1:安装基础环境(搜索引擎搜“Android开发环境搭建”)。

步骤2:导入工程。

下载随书官网中的NotePad和NotePadTest两个工程,打开Eclipse→File→Import,选择“Existing Projects into Workspace”,如图3-2所示。选择NotePad工程所在的目录,导入NotePad工程。使用相同的步骤,导入NotePadTest工程。如图3-3所示。


图3-2 选择导入已存在的工程至工作空间




图3-3 选择NotePad及NotePadTest工程所在的目录

步骤3:配置工程。

导入两个Demo工程后,由于两个工程均包含有一些配置文件,因此如果没有提示错误,则可以直接使用;如果还有提示错误,请依实际情况检查以下配置项。

(1)配置签名:两个工程需要签名一致,这里使用Android开发环境中自带的debug.keystore进行签名,因此需要确保环境中包含该签名文件,签名配置查看:Window→Preferences→Android→Build,如图3-4所示。

(2)配置build target:build target即指定使用哪个Android平台来构建这个项目,两个工程均配置为使用target=android-19,即使用sdk中platforms目录下android-19目录中的android.jar这个jar包编译项目,如图3-5所示。

若你的开发环境中未下载相应的API level的jar包,请使用SDK Manager下载,或者自行将project.properties配置文件中的target换成用户机器中已下载的API level。


图3-4 检查签名文件


图3-5 配置build target

(3)引用Robotium的jar包并关联源码:Robotium测试框架以jar包形式提供,在测试工程中引用Robotium的jar包即可。Android的Junit形式测试工程与Android工程一样,将要引用的jar包放入libs目录下,在Eclipse中将默认变成Android Private Libraries私有库,这样默认在Eclipse中就可以引用该jar中的API,在编译时也会将其编译进测试工程的APK中,如图3-6所示。

此外,为了方便查看Robotium中的源码实现,一般也会选择关联引用的jar包的源码,如上图所示,在libs中新建相应的properties文件,然后使用src = 形式的命令指定源码所在的目录即可。



图3-6 配置Robotium的jar包并关联源码

(4)配置编码:新导入工程后,由于工程里会含有一些中文注释,常常会由于编码不一致,导致代码结构被破坏而引起工程编译出错,Demo中的两个工程均采用UTF-8编码,因此需要检查导入后编码是否为UTF-8,右键工程依次选择Properties→Resource,查看编码,如图3-7所示。


图3-7 配置编码

(5)配置Instrumentation:测试工程需要配置Instrumentation以指定要注入的被测进程,即指定被测App,在NotePadTest中使用<instrumentation>标签指定targetPackage为被测应用的包名,如图3-8所示。

步骤4:运行示例。

首先确保有手机开启了USB调试,并连接了电脑,然后如图3-9所示,右键选择示例的测试类,例如右键选择NotePadTest.java类,选择Run As→Android Junit Test,即可运行Demo中的测试用例。



图3-8 配置Instrumentation


图3-9 运行示例

对于环境搭建,新手较容易出现如下问题:

常见问题1:The import android cannot be resolved

需要检查上文配置工程部分中配置的build target是否正确。

常见问题2:java.lang.NoClassDefFoundError:com.robotium.solo.Solo

NoClassDefFoundError指在编译时该类是存在的,但在运行的时候找不到该类,报找不到Solo类时一般意味着Robotium的jar包未打进测试工程的apk包中。首先,右键测试工程→Build Path→Configure Build Path,查看确保在Libraries中包含了Robotium,如图3-10所示。由于demo中将Robotium的jar包放至libs目录下了,因此默认将包含至Android Private Libraries中。

然后,如图3-11所示,在Build Path的Order and Export中确保Robotium的jar包处于勾选状态(处于勾选状态即意味着该jar的Class类将被打包进测试工程的APK中,而例如Android SDK中的android.jar包,由于其Class类在手机的Android系统中已存在,因此不需要勾选,在运行时也可以找到相应的类)。


图3-10 确保Libraries中包含Robotium


图3-11 确保Order and Export中Robotium的jar包被勾选

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;

}

3.2 Robotium原理简析

如前文所述,一个基本的自动化测试用例主要分为获取控件、控件操作、断言三个步骤,而在实际编写测试用例的过程中,我们常常会遇到各种各样的问题,比如:

在这样的UI结构下该如何获取控件?

为何报这样或那样的错?

明明滑动了为何没有效果?

因为不同的项目有其自身的独特性与复杂性,没有任何书籍可以解决实际过程中遇到的所有问题,甚至即使求助Google搜索也可能得不到自己想要的答案。因此,对于任何一门技术而言都很有必要知其然并知其所以然,只有了解了其原理实现,才能更高效地运用在实际项目中。本节将从获取控件原理、控件操作原理、WebView支持等维度来对Robotium原理进行简要解析。

3.2.1 Robotium支持Native原理

1. 获取控件原理

我们知道Android会为res目录下的所有资源分配ID,例如在布局xml文件中使用了 android:id="@+id/example_id",那么在Android工程编译时就会在R.java中相应地为该布局控件分配一个int型的ID,在Android工程中就可以通过Activity、Context或View等对象调用findViewById(int id)方法引用相应布局中的控件。因此,在测试工程中,如果是在源码的情况下,测试工程可以引用被测工程的代码,也即可以直接获得被测工程中R.java中的ID,因此可以通过这种方式直接根据ID获取控件。Robotium中根据ID获取控件的实现即包含该方式,如代码清单3-5所示。

代码清单3-5 Getter.getView

public View getView(int id, int index, int timeout){

    final Activity activity = activityUtils.getCurrentActivity(false);

    View viewToReturn = null;

    //如果index小于1,则直接通过Activity的findViewById查找

    if(index < 1){

        index = 0;

        viewToReturn = activity.findViewById(id);

    }


    if (viewToReturn != null) {

        return viewToReturn;

    }


    return waiter.waitForView(id, index, timeout);

}

在getView(int id, int index, int timeout)方法中,先获取当前所在的Activity,然后直接通过findViewById(id)方法尝试获取控件,如果该方法能够正确获取,则直接返回;否则,使用waitForView(id, index, timeout)方法进一步等待控件的出现。

对于测试工程没有关联被测工程的情况,是无法直接通过R.id.example_id的形式获取控件的,此时一般调用getView(String id)方法,即通过String型ID获取。之所以可以通过String型ID获取控件,是因为Robotium中该方法使用了Resources.getIdentifier(String name, String defType, String defPackage)方法动态地将String型ID转换成了int型ID,如代码清单3-6所示。

代码清单3-6 Getter.getView(String id,int index)

public View getView(String id, int index){

    View viewToReturn = null;

    Context targetContext = instrumentation.getTargetContext(); 

    String packageName = targetContext.getPackageName(); 

    //先将String类型的ID转换成int型的ID

    int viewId = targetContext.getResources().getIdentifier(id, "id", packageName);


    if(viewId != 0){

        viewToReturn = getView(viewId, index, TIMEOUT); 

    }

    //如果还未找到,则传入的ID可能是Android系统中的ID

    if(viewToReturn == null){

        int androidViewId = targetContext.getResources().getIdentifier(id, "id", "android");

        if(androidViewId != 0){

            viewToReturn = getView(androidViewId, index, TIMEOUT);

        }

    }


    if(viewToReturn != null){

        return viewToReturn;

    }

    return getView(viewId, index); 

}

因此,为了简化操作,我们完全可以统一使用getView(String id)方法来获取控件。

以上为根据ID获取控件的一种方式,另一种方式则是通过WindowManager获取所有View后再进行各种过滤封装。如代码清单3-7所示,在ViewFetcher中通过getAllViews方法获取所有的View,其中分别处理DecorView与nonDecorView。

代码清单3-7 ViewFetcher.getAllViews

public ArrayList<View> getAllViews(boolean onlySufficientlyVisible) {

    //获取所有的DocorViews

    final View[] views = getWindowDecorViews();

    final ArrayList<View> allViews = new ArrayList<View>();

    final View[] nonDecorViews = getNonDecorViews(views);

    View view = null;


    if(nonDecorViews != null){

        for(int i = 0; i < nonDecorViews.length; i++){

            view = nonDecorViews[i];

            try {

                addChildren(allViews, (ViewGroup)view, onlySufficientlyVisible);

            } catch (Exception ignored) {}

            if(view != null) allViews.add(view);

        }

    }


    if (views != null && views.length > 0) {

        view = getRecentDecorView(views);

        try {

            addChildren(allViews, (ViewGroup)view, onlySufficientlyVisible);

        } catch (Exception ignored) {}


        if(view != null) allViews.add(view);

    }


    return allViews;

}

如代码清单3-8所示,在getWindowDecorViews方法中通过使用反射获取Window-Manager中的mViews对象来获取所有DecorView,其中也可以看到对于Android系统版本大于19的处理是不同的。

代码清单3-8 ViewFetcher.getWindowDecorViews

@SuppressWarnings("unchecked")

public View[] getWindowDecorViews()

{

    Field viewsField;

    Field instanceField;

    try {

//通过反射获取WindowManagerGlobal或WindowManagerImpl中的mViews变量

        viewsField = windowManager.getDeclaredField("mViews");

//通过反射获取WindowManagerGlobal或WindowManagerImpl中的WindowManager实例的变量

        instanceField = windowManager.getDeclaredField(windowManagerString);

        viewsField.setAccessible(true);

        instanceField.setAccessible(true);

        Object instance = instanceField.get(null);

        View[] result;

        if (android.os.Build.VERSION.SDK_INT >= 19) {

            result = ((ArrayList<View>) viewsField.get(instance)).toArray(new View[0]);

        } else {

            result = (View[]) viewsField.get(instance);

        }

        return result;

    } catch (Exception e) {

        e.printStackTrace();

    }

    return null;

}

再看代码清单3-9中的WindowManagerString变量的来源,如代码清单3-9所示,WindowManagerString也同样地需要根据Android系统版本的不同而分别处理。

代码清单3-9 ViewFetcher.setWindowManagerString

private void setWindowManagerString(){

    //不同的系统版本,WindowManager的变量名不同

    if (android.os.Build.VERSION.SDK_INT >= 17) {

        windowManagerString = "sDefaultWindowManager";

    } else if(android.os.Build.VERSION.SDK_INT >= 13) {

        windowManagerString = "sWindowManager";

    } else {

        windowManagerString = "mWindowManager";

    }

}

至此我们知道了Robotium中获取所有Views是通过反射机制实现的,而源码中的变量很可能根据版本的不同而改变,因此通过反射则往往需根据系统版本的不同而分别处理。所以,使用Robotium时最好使用开源项目中的最新版本,因为当有新的Android系统版本发布时,很可能Robotium也需要与时俱进地完善获取控件方式。

2. 控件操作原理

Robotium获取控件后,调用clickOnView(View view)方法就可以完成点击操作,这个方法可以实现两大功能:

根据View获取了控件在屏幕中的坐标。

根据坐标发送了模拟的点击操作。

如代码清单3-10所示,由于View本身可以获取到在屏幕中的起始坐标与控件长宽,因此通过getLocationOnScreen获取起始坐标后,再加上1/2的长与宽,即可计算出控件的中心点在屏幕中的位置。

代码清单3-10 Clicker.getClickCoordinates

private float[] getClickCoordinates(View view){

    sleeper.sleep(200);

    int[] xyLocation = new int[2];

    float[] xyToClick = new float[2];

    //获取view的坐标,xyLocation[0]为x坐标的值,xyLocation[1]为y坐标的值

    view.getLocationOnScreen(xyLocation);


    final int viewWidth = view.getWidth();

    final int viewHeight = view.getHeight();

    //xyLocation中的值为控件左上角的坐标,因此xyLocation[0]+宽长除2即为该控件在x轴的中心点,同样地计算在y轴的中心点

    final float x = xyLocation[0] + (viewWidth / 2.0f);

    float y = xyLocation[1] + (viewHeight / 2.0f);


    xyToClick[0] = x;

    xyToClick[1] = y;

    return xyToClick;

}

知道了需要点击的位置后,那么接下来发送模拟点击就可以了。Android中的模拟操作可以通过MotionEvent来实现,而MotionEvent主要有以下三种形式:

MotionEvent.ACTION_DOWN:模拟对屏幕发送下按事件。

MotionEvent.ACTION_UP:模拟对屏幕发送上抬事件。

MotionEvent.ACTION_MOVE:模拟对屏幕发送移动事件。

Robotium中的点击屏幕方法即是通过MotionEvent实现的,如代码清单3-11所示,通过MotionEvent.obtain(long downTime, long eventTime, int action, float x, float y, int metaState)方法获取相应的event事件后,再通过Instrumentation的sendPointerSync(MotionEvent event)方法将event事件实际地在手机上模拟执行。

代码清单3-11 Clicker.clickOnScreen

public void clickOnScreen(float x, float y, View view) {

    boolean successfull = false;

    int retry = 0;

    SecurityException ex = null;


    while(!successfull && retry < 20) {

        long downTime = SystemClock.uptimeMillis();

        long eventTime = SystemClock.uptimeMillis();

        MotionEvent event = MotionEvent.obtain(downTime, eventTime,

            MotionEvent.ACTION_DOWN, x, y, 0);

        MotionEvent event2 = MotionEvent.obtain(downTime, eventTime,

            MotionEvent.ACTION_UP, x, y, 0);

        try{

            //通过Instrumentation模拟发送下按操作

            inst.sendPointerSync(event);

            //通过Instrumentation模拟发送上抬操作,与下按操作结合,模拟完成了一个点击过程

            inst.sendPointerSync(event2);

            successfull = true;

        }catch(SecurityException e){

            ex = e;

            dialogUtils.hideSoftKeyboard(null, false, true);

            sleeper.sleep(MINI_WAIT);

            retry++;

            View identicalView = viewFetcher.getIdenticalView(view);

            if(identicalView != null){

                float[] xyToClick = getClickCoordinates(identicalView);

                x = xyToClick[0]; 

                y = xyToClick[1];

            }

        }

    }

//如果点击失败,将抛出异常

    if(!successfull) {

        Assert.fail("Click at ("+x+", "+y+") can not be completed! ("+(ex != null ? ex.getClass().getName()+": "+ex.getMessage() : "null")+")");

    }

}

结合getClickCoordinates(View view)与clickOnScreen(float x, float y, View view)方法就完成了clickOnView(View view)方法的核心实现。通过控制不同手势操作的时间顺序还可以模拟各种手势操作,例如先发送MotionEvent.ACTION_DOWN,一段时间后,再发送MotionEvent.ACTION_UP就模拟了长按操作。先发送MotionEvent.ACTION_DOWN,然后发送MotionEvent.ACTION_MOVE,最后发送MotionEvent.ACTION_UP就是滑动操作了。因此,结合MotionEvent的各种模拟事件也可以自行实现自定义的手势操作。

3.2.2 Robotium支持WebView原理

在上一节中我们介绍了在Robotium中如何通过By.id或By.className方式获取Web-Element,那么Robotium中是如何获取到相应的HTML元素,并能知道元素坐标,从而发送点击事件的呢?

1. WebElement对象

Robotium中以WebElement对象对HTML元素进行了封装,在这个WebElement对象中包含locationX、locationY、ID、text、name、className、tagName等信息。

locationX、locationY:标识该HTML元素在屏幕中所在的X坐标和Y坐标。

ID、className:该HTML元素的属性。

tagName:该HTML元素的标签。

Robotium中封装了WebElement,提供了clickOnWebElement(WebElement webElement),ArrayList<WebElement> getCurrentWebElements()等操作Web元素的API,对于在Android客户端中展示的Web页面,Robotium是如何把里面的元素都提取出来,并封装进WebElement对象中的呢?

如图3-13所示,通过getWebElements方法的调用关系图可以看出,Robotium主要通过JS注入的方式获取Web页面所有的元素,再对这些元素进行提取并封装成WebElement对象。在Android端与JS交互则离不开WebView和WebCromeClient。



图3-13 getWebElements方法的调用关系图

2. WebElement元素获取

1)利用JS获取页面中的所有元素

在PC上,获取网页的元素可以通过注入javascript元素来完成,以Chrome浏览器为例,打开工具—JavaScript控制台(快捷方式:Ctrl+Shift+J键),输入javascript:prompt (document.URL)即会弹出含当前页面的URL的提示框,因此通过编写适当的JS脚本就可以在这个弹出框中显示所有的页面元素。RobotiumWeb.js就提供了获取所有HTML元素的JS脚本。以Solo中getWebElements()为例,如代码清单3-12所示,可分为两步,先通过executeJavaScriptFunction()方法执行JS脚本,然后根据执行结果通过getWebElements返回。

代码清单3-12 WebUtils.getWebElements

public ArrayList<WebElement> getWebElements(boolean onlySufficientlyVisible){

    boolean javaScriptWasExecuted = executeJavaScriptFunction("allWebElements();");


    return getWebElements(javaScriptWasExecuted, onlySufficientlyVisible);

}

如代码清单3-13所示,在executeJavaScriptFunction(final String function)方法中通过webView.loadUrl(String url)方法执行JS,而这里的WebView是通过getCurrentViews (Class<T> classToFilterBy, boolean includeSubclasses)过滤出来的,且是过滤的android.webkit.WebView,这也是Robotium只支持系统WebView而不支持第三方浏览内核中的WebView的原因:

代码清单3-13 WebUtils.executeJavaScriptFunction

private boolean executeJavaScriptFunction(final String function) {

    List<WebView> webViews = viewFetcher.getCurrentViews(WebView.class, true);

    //获取当前屏幕中最新的WebView,即目标要执行JS的WebView

    //注:这里获取的WebView可能不是目标WebView,那么将导致获取WebElement失败

    final WebView webView = viewFetcher.getFreshestView((ArrayList<WebView>) webViews);

    

    if(webView == null) {

        return false;

    }

    //执行JS前的准备工作,如设置WebSettings、获取JS方法等

    final String javaScript = setWebFrame(prepareForStartOfJavascriptExecution(webViews));

        

    inst.runOnMainSync(new Runnable() {

        public void run() {

            if(webView != null){

    //调用loadUrl执行JS

                webView.loadUrl("javascript:" + javaScript + function);

            }

        }

    });

    return true;

}

想返回什么样的结果,关键在于执行了什么样的JS方法,Robotium中的getWeb-Elements()执行的JS方法是allWebElements(),代码片段可以通过RobotiumWeb.js找到,如代码清单3-14所示,采用遍历DOM的形式获取所有的元素信息。

代码清单3-14 RobotiumWeb.js中的allWebElements()

function allWebElements() {

    for (var key in document.all){

        try{

            promptElement(document.all[key]);           

        }catch(ignored){}

    }

    finished();

}

如代码清单3-15所示,将代码清单3-15中遍历获取到的每一个元素分别获取ID、text、className等,然后将元素通过prompt方法以提示框形式显示。在prompt时,会在ID、text、className等字段之间加上';,'特殊字符,以便解析时区分这几个字段。

代码清单3-15 RobotiumWeb.js中的promptElement(element)

function promptElement(element) {

    var id = element.id;

    var text = element.innerText;

    if(text.trim().length == 0){

        text = element.value;

    }

    var name = element.getAttribute('name');

    var className = element.className;

    var tagName = element.tagName;

    var attributes = "";

    var htmlAttributes = element.attributes;

    for (var i = 0, htmlAttribute; htmlAttribute = htmlAttributes[i]; i++){

        attributes += htmlAttribute.name + "::" + htmlAttribute.value;

        if (i + 1 < htmlAttributes.length) {

            attributes += "#$";

        }

    }


    var rect = element.getBoundingClientRect();

    if(rect.width > 0 && rect.height > 0 && rect.left >= 0 && rect.top >= 0){

        prompt(id + ';,' + text + ';,' + name + ";," + className + ";," + tagName + ";," + rect.left + ';,' + rect.top + ';,' + rect.width + ';,' + rect.height + ';,' + attributes);

    }

}

最后,执行finished()方法,调用prompt提示框,提示语为特定的'robotium-finished',用于在Robotium执行JS时,判断是否执行完毕,如代码清单3-16所示。

代码清单3-16 RobotiumWeb.js中的finished()

function finished(){

    //robotium-finished用来标识Web元素遍历结束

    prompt('robotium-finished');

}

通过JS完成了Web页面所有元素的提取,提取的所有元素是以prompt方式显示在提示框中的,那么提示框中包含的内容在Android中怎么获取呢?

2)通过onJsPrompt回调获取prompt提示框中的信息

如代码清单3-17所示,通过JS注入获取到Web页面所有的元素后,可以通过onJsPrompt回调来对这些元素进行提取。Robotium写了个继承自WebChromeClient类的RobotiumWebClient类,覆写了onJsPrompt用于回调提取元素信息,如果提示框中包含“robotium-finished”字符串,即表示这段JS脚本执行完毕了,此时通知webElementCreator可以停止等待,否则,将不断将prompt框中的信息交由webElementCreator.createWeb-ElementAndAddInList解析处理。

代码清单3-17 RobotiumWebClient中的onJsPrompt

@Override

public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult r) {

    //当message包含robotium-finished时,表示JS执行结束

    if(message != null && (message.contains(";,") || message.contains("robotium-finished"))){

   

        if(message.equals("robotium-finished")){

    //setFinished为true后,WebElementCreator将停止等待

            webElementCreator.setFinished(true);

        }

        else{

            webElementCreator.createWebElementAndAddInList(message, view);

        }

        r.confirm();

        return true;

    }

    else {

        if(originalWebChromeClient != null) {

            return originalWebChromeClient.onJsPrompt(view, url, message, defaultValue, r); 

        }

        return true;

    }


}

3)将回调中获取的元素信息封装进WebElement对象中

获取到onJsPrompt回调中的元素信息后,接下来就可以对这些已经过处理、含特殊格式的消息进行解析了,依次得到WebElement的ID、text、name等字段。如代码清单3-18所示,将information通过特殊字符串“;,”分隔成数组对该字符串进行分段解析,将解析而得的ID、text、name及x,y坐标存储至WebElement对象中。

代码清单3-18 WebElementCreator中的createWebElementAndSetLocation

private WebElement createWebElementAndSetLocation(String information, WebView webView){

    //将information通过特殊字符串“;,”分隔成数组

    String[] data = information.split(";,");

    String[] elements = null;

    int x = 0;

    int y = 0;

    int width = 0;

    int height = 0;

    Hashtable<String, String> attributes = new Hashtable<String, String>();

    try{

        x = Math.round(Float.valueOf(data[5]));

        y = Math.round(Float.valueOf(data[6]));

        width = Math.round(Float.valueOf(data[7]));

        height = Math.round(Float.valueOf(data[8]));    

        elements = data[9].split("\\#\\$");

    }catch(Exception ignored){}


    if(elements != null) {

        for (int index = 0; index < elements.length; index++){

            String[] element = elements[index].split("::");

            if (element.length > 1) {

                attributes.put(element[0], element[1]);

            } else {

                attributes.put(element[0], element[0]);

            }

        }

    }

    WebElement webElement = null;

    try{

    //设置WebElement中的各个字段

        webElement = new WebElement(data[0], data[1], data[2], data[3], data[4], attributes);

        setLocation(webElement, webView, x, y, width, height);

    }catch(Exception ignored) {}


    return webElement;

}

这样,把JS执行时提取到的所有元素信息解析出来,并储存至WebElement对象中,在获取到相应的WebElement对象后,就包括了元素的ID、text、className等属性及其在屏幕中的坐标,完成了对Web自动化的支持。

3.3 Robotium实践运用

3.3.1 控件ID相同时获取控件

实际界面中常常有一些子控件是相同ID甚至没有ID的,但这时候一般其父视图是有ID的。如图3-14所示,每个TAB的控件ID是相同的。


图3-14 拥有相同ID的底部TAB

因为界面中也很可能会出现多个发现、游戏这样的文本,因此也不能采取类似getText(“发现”)这样的方式。这里,我们就可以通过ID获取唯一父控件,再通过过滤方式获取指定的控件。

//先根据ID获得唯一的布局LinearLayout

LinearLayout mTabs = (LinearLayout)solo.getView("main_tabs");

//然后通过过滤方式获取该LinearLayout下的所有文本控件

ArrayList<TextView> tabs = solo

              .getCurrentViews(TextView.class,mTabs);

如果子控件的ID都是一样的,而我们仍然希望通过ID来定位控件,那么应该如何获取呢?我们知道不论是Activity类还是View类都是可以通过findViewById(int id)方法直接在控件树中根据ID来查找控件的,因此当我们获得一个父视图后,就可以通过findViewById (int id)方法根据ID来查找相应的子控件,这种方法可以普遍应用在ListView中。

//先根据ID获得唯一的布局ListView

ListView mListView = (ListView)solo.getView("example_list_id");

//先通过mListView.getChildAt(0)获取该ListView的第一个child,然后再通过该

//child在控件树中使用findViewById根据ID来获取

TextView firstListTitle = (TextView) mListView.getChildAt(0).findViewById(getId ("example_title"));

这里的重点是findViewById(int id)传进去的是int型的ID,而我们通过hierarchyviewer或uiautomatorviewer查看到的ID都是String型的,由前文的原理介绍可知,我们可以将String型的ID转换成int型的ID,如代码清单3-19所示:

代码清单3-19 将String型的ID转换成int型的ID

public int getId(String id,String packageName){

    Context targetContext = instrumentation.getTargetContext().getApplicationContext();

    int viewId = targetContext.getResources().getIdentifier(id, "id", packageName);

    LogUtils.logD("CopyOfAssistantTabActivityTest", "viewId:" + viewId);

    if(viewId == 0){

        viewId = targetContext.getResources().getIdentifier(id, "id", "android");

    }

    

    return viewId;

}

因此,当碰到同一层级控件ID相同时,可以先寻找唯一的父布局,再通过父布局寻找子控件。如果子控件结构均相同,那么可以通过index索引来查找;如果子控件结构不一致,则可以通过遍历的方式找到指定的子控件。

3.3.2 ListView列表遍历 

编写Android端的自动化测试用例,最常见的控件有ListView,而要想测试ListView,就必然要涉及ListView的遍历。

关于ListView的遍历,可能首先想到的是类似如代码清单3-20的实现方式。

代码清单3-20 设想中的列表遍历 

for(int i=0;i<listView. getCount();i++){

    listView.getChildAt(int index);

    ……;

}

但是,在Android中,对于listView.getChildAt(int index)而言,如果子控件是在屏幕之外的话,那么是无法点击的,因此要想点击或测试屏幕之外的子控件,就需要不断向上滑动。因此我们可以先遍历当前屏幕内的子控件,然后翻一屏,再遍历屏幕内的子控件,如此反复就可以遍历ListView所有的子控件了。

对于ListView而言,通过getFirstVisiblePosition()和getLastVisiblePosition()可以获取ListView在屏幕中第一个可见子控件及最后一个可见子控件在列表中的位置。当遍历至当前最后一个子控件时,通过solo.scrollListToLine(listView, lastPosition)方法将列表滑至lastPosition所在的位置,即实现翻屏的效果。当遍历至每个child子控件时,可以通过该子控件的布局结构来判断该子控件是否为要查找的控件。另外,需要注意的是,正如前文所介绍的,scrollListToLine(listView, lastPosition)方法并不会直接产生上滑手势,因此如果列表需要产生上滑动作才能加载更多的话,则还需要配合使用drag方法进行上拉加载更多。

如代码清单3-21所示,遍历列表,查找列表中子节点为RelativeLayout且子节点的标题为×××的子控件。

代码清单3-21 遍历列表并找到指定标题的child 

public RelativeLayout findCardByType(int maxCount) {

    // 获取当前界面中的ListView

    ListView listView = getCurrentListView();

    int firstPosition = 0;

    int lastPosition = 0;

    RelativeLayout relativeLayout = null;

    int currentPosition = 1;

    labelAll: 

    for (int i = 0; i < length; i++) {

        firstPosition = listView.getFirstVisiblePosition();

        lastPosition = listView.getLastVisiblePosition();

        for (int j = 1; j <= lastPosition - firstPosition; j++) {

            currentPosition++;

            if (currentPosition >= maxCount) {

                break labelAll;

            }

            // 判断该节点是否为relativeLayout

            if (listView.getChildAt(j) instanceof RelativeLayout) {

                relativeLayout = (RelativeLayout) listView.getChildAt(j);

                // 这里可以对该relativeLayount进行判断,例如获取该//relativeLayout中的子控件,如果有标题则判断标题等

                if (isSatisfied(relativeLayout)) {

                    break labelAll;

                }

                relativeLayout = null;

            }

        }

        solo.scrollListToLine(listView, lastPosition);

        if (lastPosition >= listView.getCount()) {

            // 当需要上拉加载更多时,调用drag实现的方法进行上拉加载更多

            dragUpToShowAll(listView);

        }

        sleeper.sleep();

    }

    sleeper.sleep();

    return relativeLayout;

}

3.3.3 修改Robotium以支持X5WebView 

本节中的X5WebView指QQ浏览器团队出品的腾讯X5内核中的WebView。除了QQ、微信、应用宝等众多腾讯内部产品在使用X5内核外,京东、58同城等众多腾讯外部的合作伙伴也在使用X5内核。

腾讯X5网站:http://x5.tencent.com/。

然而Robotium本身并不支持获取X5WebView中的元素,因此无法对使用了X5内核的Web页面进行自动化测试,而通过3.2.2节中介绍的Robotium支持WebView原理可知,只要对Robotium稍加改造,即可使用同样的原理获取WebElement对象,完成对X5WebView自动化的支持。

这里再概述一下Robotium支持WebView的过程,以便理解为何Robotium不支持X5以及如何修改。

步骤1:获取目标WebView。

如代码清单3-13所示,代码final WebView webView = viewFetcher.getFreshestView (viewFetcher.getCurrentViews(WebView.class));调用ViewFetcher类获取当前界面中的WebView,而该WebView是android.webkit.WebView。

步骤2:做执行JS前的准备工作。

如代码清单3-13所示,final String javaScript = prepareForStartOfJavascriptExecution();调用prepareForStartOfJavascriptExecution(),该方法还调用了如代码清单3-22所示的代码,将WebSettings是否允许执行JS设置为True(系统默认是False)。然后还设置了WebView的WebChromeClient(WebChromeClient用于辅助WebView处理Javascript的对话框、提示框等)。从这里可以看出Robotium使用的是继承自android.webkit.WebChromeClient的RobotiumWebClient。

代码清单3-22 RobotiumWebClient.enableJavascriptAndSetRobotiumWebClientd

/**

 * Enables JavaScript in the given {@code WebViews} objects.

 * 

 * @param webViews the {@code WebView} objects to enable JavaScript in

 */


public void enableJavascriptAndSetRobotiumWebClient(List<WebView> webViews, WebChromeClient originalWebChromeClient){

    this.originalWebChromeClient = originalWebChromeClient;


    for(final WebView webView : webViews){


        if(webView != null){ 

            inst.runOnMainSync(new Runnable() {

                public void run() {

                   //WebSettings开启JS

                    webView.getSettings().setJavaScriptEnabled(true);

                    webView.setWebChromeClient(robotiumWebClient);


                 }

            });

        }

    }

}

步骤3:在指定WebView中执行相应JS。

如代码清单3-13所示,最后调用webView.loadUrl("javascript:" + javaScript + function);方法在指定的WebView中执行相应片段的JS代码。

从以上核心步骤中可以看出,Robotium不支持X5的原因在于,首先,其获取目录WebView时,是获取android.webkit.WebView中的WebView;其次,辅助处理JS的WebChromeClient也是继承自android.webkit.WebChromeClient。而X5内核中的WebView并不是继承自android.webkit.WebView,X5内核中的WebChromeClient也不是继承自android.webkit.WebChromeClient,因此Robotium没法获取X5内核中的目标WebView,也就没法在目标WebView中执行JS并提取WebElement元素。了解个中缘由后,就可以稍加改造以支持X5WebView。

如图3-15所示为以外部引用(即该jar包的类并不实际打包进测试工程,仅在IDE调试时用。当调用相应的类时,寻找的是被测工程中的相应的类)的方式导入X5提供的SDK。


图3-15 导入X5提供的SDK

在获取目标WebView时,相应地修改成X5 SDK中的WebView。如图3-16所示,获取目标WebView时修改为com.tencent.smtt.sdk.WebView。


图3-16 修改目标WebView 

同样地,修改WebChromeClient为继承自com.tencent.smtt.sdk.WebChromeClient中的TxWebChromeClient,然后在WebView中设置WebChromeClient时使用TxWebChromeClient,如图3-17所示。


图3-17 修改目标WebChromeClient 

对于其他有相应的WebView或WebChromeClient调用的地方,均修改成X5 SDK中对应的WebView及WebChromeClient,修改完成后,将相应的类带上前缀以便区分,如图3-18所示。


图3-18 修改后的类 

当需要获取使用了X5内核的Web元素时,调用TxWebUtils类中的相应方法即可。如图3-19所示,与Robotium原有的WebUtils使用方法一致,至此,完成了对X5内核的支持。


图3-19 TxWebUtils中的类方法

3.4 本章小结

本章分三小节,从功能、原理及实践三方面介绍了Robotium测试框架,第一小节先全面概览似的介绍了Robotium的整体,然后从控件获取、控件操作、WebView支持、断言等维度介绍了相应功能及其使用方法,力图让读者知道如何使用Robotium测试框架来进行用例编写。第二小节则分别从Native和Web角度介绍了Robotium的实现原理,力图让读者了解更多的为什么,从而可以在实际项目中更灵活地使用Robotium编写测试用例。第三小节则从实践运用角度选取一般项目中常见的一些场景,介绍使用Robotium处理的思路与方法。

本章主要从测试用例编写过程这一思维主线来介绍Robotium测试框架,从中也可以看出,不论是对获取复杂控件还是进行各类模拟操作,Robotium均可以很好地支持,且也可以支持App中的Web自动化,基本可以满足日常的自动化测试需求。当然,Robotium也有如跨应用能力弱等固有劣势,实际上也并没有哪一款测试框架可以解决所有遇到的测试问题,我们大可结合不同的测试框架、测试工具来解决实际项目中的问题。因此,Robotium可以说是一款不可多得的优秀的自动化测试框架。


上一篇:[android]android自动化测试十四之dumpsys性能测试


下一篇:@RequestParam和@PathVariable的区别及其应用场景