android应用测试与调试实战


移动开发 Android 应用测试与调试实战 施懿民 著 图书在版编目(CIP)数据 Android 应用测试与调试实战 / 施懿民著 . —北京:机械工业出版社,2014.3 (移动开发) ISBN 978-7-111-46018-3 I. A… II. 施… III. 移动终端 – 应用程序 – 程序设计 IV. TN929.53 中国版本图书馆 CIP 数据核字(2014)第 041414 号 本书是 Android 应用测试与调试领域最为系统、深入且极具实践指导意义的著作,由拥有近 10 年从业经验 的资深软件开发工程师和调试技术专家撰写,旨在为广大程序员开发高质量的 Android 应用提供全方位指导。它 从 Android 应用自动化测试工程师和开发工程师的需求出发,从测试和调试两个维度,针对采用 Java、HTML 5、 C++&NDK 三种 Android 应用开发方式所需要的测试和调试技术、方法进行了细致而深入的讲解,为 Android 应用 的自动化测试和调试提供原理性的解决方案。 全书一共 16 章,分为两大部分:第一部分为自动化测试篇(第 1~11 章),详细讲解了进行Android 自动化测试 需要掌握的各种技术、工具和方法,包括 Android 自动化测试基础、Android 应用的白盒自动化测试和黑盒自动化测 试的技术和原理、Android 服务组件和内容组件的测试、HTML 5 应用和 NDK 应用的测试,以及 Android 应用的兼 容性测试和持续集成自动化测试;第二部分为调试技术篇(第 12~16 章),详细讲解了 Android 应用调试所需要的各 种工具的使用、操作日志的分析、内存日志的分析,以及多线程应用 HTML 5 应用和 NDK 应用的调试方法和技巧。 Android 应用测试与调试实战 施懿民 著 出版发行:机械工业出版社(北京市西城区百万庄大街 22 号 邮政编码:100037) 责任编辑:姜 影 印  刷: 版  次:2014 年 4 月第 1 版第 1 次印刷 开  本:186mm×240mm 1/16 印  张:27.75 书  号: ISBN 978-7-111-46018-3 定  价:79.00 元 凡购本书,如有缺页、倒页、脱页,由本社发行部调换 客服热线:(010)88378991 88361066 投稿热线:(010)88379604 购书热线:(010)68326294 88379649 68995259 读者信箱:hzjsj@hzbook.com 版权所有· 侵权必究 封底无防伪标均为盗版 本书法律顾问:北京大成律师事务所 韩光 / 邹晓东 前  言 为什么要写这本书 几年前看过微软工程师 Adam Nathan 写的一本书《 .NET and COM: The Complete Interoperability Guide》,对作者说过的一句话印象极其深刻:学习一门新技术的最佳方法就是写一本书。对于这个 说法,笔者深以为然,在实际工作中,迫于项目交付的压力,很多时候只会使用一门技术中自己熟 悉的部分,对于其他部分,甚至自己熟悉的那一部分,也只是知其然而不知其所以然,似懂非懂。 而当要写一本书介绍这门技术的时候,就不得不去上下求索,这时不仅要横向了解该门技术的各个 细节,还要纵向了解该门技术的发展思路以及各部分的来龙去脉,才能向读者解释它。因此可以 说,写作本书的过程也是笔者系统学习 Android 操作系统的过程。 而笔者之所以从测试和调试的角度来介绍 Android 操作系统,是因为从这两个角度入手能够很 好地从横向和纵向两个方向学习 Android 系统。从测试的角度来说,由于一般测试人员对 Android 应用进行集成测试和系统测试,开发人员负责单元测试工作,这就要求测试人员,特别是自动化测 试人员对 Android 应用所涉及的技术有一个广度的认知,对 Android 的各种技术都要知其然。从调 试的角度来说,却又可以从源码级别知各项技术的所以然。比如,要调试应用的功能错误,就必须 对应用的源码有一个完整的理解,只有这样才能知道应该在什么地方设置断点,在什么时候让调试 器捕捉异常;而要调试应用的内存泄露问题,就必须对 Android 系统采用的垃圾回收机制有一个通 透的理解,只有这样才能从根源上发现并解决内存方面的问题。另外,反观市面上的技术书籍,介 绍自动化测试和调试技术的书实在少。但在软件开发过程中,不仅测试是必不可少的环节,而且 程序员实际上只花 20% 的时间写代码并完成编译过程,剩下 80% 的时间都是花在调试和修改代码 上,这种情况也坚定了笔者写作本书的信心。 最后,由于笔者能力有限,同时为了及时出版本书,不得不放弃一些如多核设备上并行程序的 调试、NDK 程序的验尸调试等内容,希望感兴趣的读者可以完成这方面的分享。 IV 读者对象 Android 自动化测试工程师 ❏ Android 开发工程师 ❏ 无编程基础的 Android 测试工程师 ❏ 如何阅读本书 虽然本书讲解的是自动化测试和程序调试相关技术,但有些测试工程师的行业经验更丰富些, 而编程基础则相对薄弱。因此本书分为两大部分: 第一部分为自动化测试篇(第 1 ~ 11 章),这一部分列举 Android 自动化测试中可以使用的 几种测试技术,尽可能详细地介绍了 Android 白盒、黑盒自动化测试所用到的技术及其原理。由 于 Android 应用可以使用 Java 语言配合 SDK,也可以用 HTML 5 技术,还可以用 C/C++ 语言配合 NDK 技术编写,所以这部分尽量涵盖了针对这三种技术编写的应用所采用到的测试技术。这些内 容适合 Android 自动化测试工程师和对自动化测试感兴趣的手工测试工程师阅读。这一部分除了第 11 章需要有 C/C++ 编程经验之外,其他章节无需编程基础即可阅读。另外每个章节都是独立的, 读者可以根据自己的实际需要分开来阅读。 第二部分为调试技术篇(第 12 ~ 16 章),第 12 章讲解的是通用的调试技术,这部分对于 Android 自动化测试工程师和 Android 开发工程师都是必要的知识,这一章节无需编程知识即可掌 握。而第 13 章之后,主要涉及的是性能方面的调试技术,其中涉及一些 Android 系统内部的实现 细节,这些技术更适合具备一定开发经验的自动化测试和开发工程师阅读。 勘误和支持 由于作者的水平有限,加之编写时间仓促,书中难免会出现一些错误或者不准确的地方,恳 请读者批评指正。为此,作者特意在 Google+ 上创建一个在线支持与应急方案的社区 https://plus. google.com/u/0/communities/112928495323595574856。 你 可 以 将 书 中 的 错 误 发 布 在 Bug 勘 误 表 页面中,同时如果你遇到任何问题,也可以访问 Q&A 页面,作者将尽量在线上提供满意的解答。 书中的全部源文件除可以从华章网站 一 下载外,还可以从这个社区和 github 上 https://github.com/ shiyimin/androidtestdebug 下载,作者也会将相应的功能更新及时发布出来。如果你有更多的宝贵 意见,也欢迎发送邮件至邮箱 shiyimin.aaron@gmail.com,期待能够得到你们的真挚反馈。 一  参见华章网站 www.hzbook.com。——编辑注 V 致谢 首先要感谢支付宝的陈晔(新浪微博:@Monkey 陳曄曄),在本书写作时,他参与了大部分章 节的技术审阅工作,帮助完善了书中的细节。 感谢阿里集团的梁剑钊(新浪微博:@liangjz)推荐的玄黎(新浪微博:@ 浪头)、李子乐(新 浪微博:@ 子乐 _ 淘宝太禅)参与本书技术审阅工作。 感谢机械工业出版社华章公司的编辑杨福川老师,在这一年多的时间中始终支持我的写作,他 的鼓励和帮助引导我顺利完成全部书稿。 谨以此书献给我最亲爱的家人,以及众多热爱 Android 测试的朋友们! 施懿民 目  录 前言 第 1 章 Android 自动化测试初探 1 1.1  快速入门 1 1.2  待测示例程序 2 1.3  第一个 Android 应用测试工程 6 1.4  搭建自动化开发环境 12 1.4.1  安装 Eclipse 和 ADT 开发包 12 1.4.2  创建模拟器 13 1.4.3  启动模拟器 21 1.4.4  连接模拟器 23 1.4.5  连接手机 24 1.5  本章小结 29 第 2 章 Android 自动化测试基础 30 2.1  Java 编程基础 30 2.2  JUnit 简介 36 2.2.1  添加测试异常情况的 测试用例 41 2.2.2  测试集合 43 2.2.3  测试准备与扫尾函数 45 2.2.4  自动化测试用例编写 注意事项 47 2.3  Android 应用程序基础 47 2.3.1  Android 权限系统 47 2.3.2  应用的组成与激活 51 2.3.3  清单文件 54 2.3.4  Android 应用程序的单 UI 线程模型 56 2.4  本章小结 57 第 3 章  Android 界面自动化 白盒测试 58 3.1  Instrumentation 测试框架 58 3.1.1  Android 仪表盘测试工程 58 3.1.2  仪表盘技术 60 3.1.3  Instrumentation. ActivityMonitor 嵌套类 63 3.2  使用仪表盘技术编写测试用例 64 3.2.1  ActivityInstrumentationTest- Case2 测试用例 66 3.2.2  sendKeys 和 sendRepeatedKeys 函数 70 3.2.3  执行仪表盘测试用例 72 3.2.4  仪表盘测试技术的限制 74 3.3  使用 robotium 编写集成测试 用例 77 3.3.1  为待测程序添加 robotium 用例 77 VII 3.3.2  测试第三方应用 80 3.3.3  robotium 关键源码解释 84 3.4  Android 自动化测试在多种 屏幕下的注意事项 87 3.5  本章小结 90 第 4 章  Android 界面自动化 黑盒测试 91 4.1  monkey 工具 91 4.1.1  运行 monkey 93 4.1.2  monkey 命令选项参考 97 4.1.3  monkey 脚本 98 4.1.4  monkey 服务器 105 4.2  编写 monkeyrunner 用例 109 4.2.1  为待测程序录制和 回放用例 110 4.2.2  运行 monkeyrunner 110 4.2.3  手工编写 monkeyrunner 代码 111 4.2.4  编写 monkeyrunner 插件 114 4.3  本章小结 118 第 5 章 测试 Android 服务组件 119 5.1  JUnit 的模拟对象技术 119 5.2  测试服务对象 128 5.2.1  服务对象简介 128 5.2.2  在应用中添加服务 130 5.2.3  测试服务对象 136 5.3  本章小结 140 第 6 章  测试 Android 内容供应 组件 142 6.1  控制反转 142 6.1.1  依赖注入 144 6.1.2  服务定位器 146 6.2  内容供应组件 147 6.2.1  统一资源标识符 150 6.2.2  MIME 类型 152 6.2.3  内容供应组件的虚拟表 视图 152 6.3  内容供应组件示例 154 6.4  测试内容供应组件 159 6.5  本章小结 163 第 7 章  测试 Android HTML 5 应用 164 7.1  构建 Android HTML 5 应用 164 7.1.1  WebView 应用 164 7.1.2  使用视口适配 Android 设备的多种分辨率 170 7.1.3  使用 CSS 适配多种 分辨率 175 7.1.4  使用 Chrome 浏览器模拟 移动设备浏览器 176 7.2  使用 QUnit 测试 HTML 5 网页 177 7.2.1  QUnit 基础 177 7.2.2  QUnit 中的断言 179 7.2.3  测试回调函数 181 7.2.4  测试 WebView 应用 182 7.3  本章小结 185 第 8 章  使用 Selenium 测试 HTML 5 浏览器应用 186 8.1  Selenium 组成部分 186 8.2  安装 Selenium IDE 187 VIII 8.3  Selenium IDE 界面 188 8.3.1  菜单栏 188 8.3.2  工具栏 189 8.4  使用 Selenium 189 8.4.1  使用 Selenium IDE 录制 测试用例 189 8.4.2  运行 Selenium 测试用例 194 8.4.3  等待操作完成 199 8.4.4  Selenium WebDriver 命令 200 8.5  数据驱动测试 206 8.6  Selenium 编程技巧 208 8.6.1  在测试代码中硬编码 测试数据 208 8.6.2  重构 Selenium IDE 生成 的代码 209 8.7  本章小结 212 第 9 章 Android NDK 测试 213 9.1  安装 NDK 213 9.2  NDK 的基本用法 214 9.3  编译和部署 NDK 示例程序 214 9.4  Java 与 C/C++ 之间的交互 217 9.4.1  Makefiles 222 9.4.2  动态模块和静态模块 222 9.5  在 Android 设备上执行 NDK 单元测试 223 9.6  unittest++ 使用基础 228 9.6.1  添加新测试用例 228 9.6.2  测试用例集合 229 9.6.3  验证宏 229 9.6.4  数组相关的验证宏 230 9.6.5  设置超时 230 9.7  本章小结 231 第 10 章 Android 其他测试 232 10.1  Android 兼容性测试 232 10.1.1  运行 Android 兼容性 测试用例集合 232 10.1.2  兼容性测试计划说明 237 10.1.3  添加一个新的测试 计划 238 10.1.4  添加一个新的测试 用例 239 10.1.5  调查 CTS 测试失败 241 10.2  Android 脚本编程环境 243 10.2.1  Android 脚本环境简介 243 10.2.2  安装 SL4A 243 10.2.3  为 SL4A 安装脚本引擎 244 10.2.4  编写 SL4A 脚本程序 246 10.2.5  在 PC 上调试脚本程序 250 10.3  国际化测试 251 10.4  模拟来电中断测试 254 10.5  本章小结 255 第 11 章 持续集成自动化测试 257 11.1  在 Ant 中集成 Android 自动化测试 257 11.1.1  Ant 使用简介 257 11.1.2  Android 应用编译过程 262 11.1.3  使用 Ant 编译 Android 工程 263 11.2  在 Maven 中集成 Android 自动化测试 268 11.2.1  使用 Android Maven Archetypes 创建新 Android 工程 268 11.2.2  Android Maven 工程 介绍 270 IX 11.2.3  与设备交互 271 11.2.4  与模拟器交互 272 11.2.5  集成自动化测试 274 11.3  收集代码覆盖率 276 11.4  本章小结 280 第 12 章 Android 功能调试工具 281 12.1  使用 Eclipse 调试 Android 应用 281 12.1.1  Eclipse 调试技巧 282 12.1.2  使用 JDB 调试 294 12.1.3  设置 Java 远程调试 296 12.1.4  调试器原理简介 301 12.2  查看 Android 的 logcat 日志 302 12.2.1  过滤 logcat 日志 303 12.2.2  查看其他 logcat 内存日志 304 12.3  Android 调试桥接 304 12.3.1  adb 命令参考 306 12.3.2  执行 Android shell 命令 309 12.3.3  dumpsys 312 12.4  调试 Android 设备上的程序 317 12.4.1  调试命令行程序 317 12.4.2  调试 Android 应用 318 12.4.3  调试 Maven Android 插件启动的应用 321 12.5  本章小结 322 第 13 章  Android 性能测试之 分析操作日志 323 13.1  使用 Traceview 分析操作 日志 326 13.1.1  记录应用操作日志 326 13.1.2  Traceview 界面说明 328 13.1.3  使用 Traceview 分析并 优化性能瓶颈 329 13.2  使用 DDMS 334 13.2.1  使用 DDMS 335 13.2.2  DDMS 与调试器交互的 原理 336 13.2.3  三种启动操作日志 记录功能的方法 338 13.3  使用 dmtracedump 分析函数 调用树 339 13.4  本章小结 341 第 14 章 分析 Android 内存问题 343 14.1  Android 内存管理原理 343 14.1.1  垃圾内存回收算法 343 14.1.2  GC 发现对象引用的 方法 351 14.1.3  Android 内存管理源码 分析 352 14.1.4  Logcat 中的 GC 信息 361 14.2  调查内存泄露工具 362 14.2.1  Shallow size 和 Retained size 362 14.2.2  支配树 363 14.3  分析 Android 内存泄露实例 364 14.3.1  在 DDMS 中检查示例 问题程序的内存情况 366 14.3.2  使用 MAT 分析内存 泄露 368 14.3.3  弱引用 372 14.3.4  MAT 的其他界面使用 方法 373 X 14.3.5  对象查询语言 OQL(Object Query Language) 376 14.3.6  使用 jHat 分析内存 文件 381 14.4  显示图片 382 14.4.1  Android 应用加载 大图片的最佳实践 386 14.4.2  跟踪对象创建 388 14.5  频繁创建小对象的问题 390 14.6  Finalizer 的问题 393 14.7  本章小结 394 第 15 章  调试多线程和 HTML 5 应用 395 15.1  调试应用无响应问题 395 15.2  Android 中的多线程 397 15.3  调试线程死锁 400 15.3.1  资源争用问题 400 15.3.2  线程同步机制 405 15.3.3  解决线程死锁问题 406 15.4  StrictMode 410 15.4.1  在应用中启用 StrictMode 413 15.4.2  暂时禁用 StrictMode 415 15.5  调试 Android 上的 浏览器应用 416 15.5.1  在 Android 系统自带的 浏览器上调试 416 15.5.2  在 Chrome 浏览器上 调试 418 15.6  本章小结 422 第 16 章  调试 NDK 程序 423 16.1  使用 Eclipse 调试 Android NDK 程序 423 16.2  在命令行中调试 NDK 程序 426 16.3  Android 的 C/C++ 调试器的 工作原理 431 16.3.1  调试符号 433 16.3.2  源码 433 16.3.3  多线程调试的问题 433 16.4  本章小结 434 第 1 章 Android 自动化测试初探 本章的目的是让有经验的测试开发工程师迅速上手 Android 自动化测试代码的编写流 程。因为大部分 Android 程序都是基于 SDK 开发,所以本章只介绍为基于 Android SDK 的 应用编写自动化测试代码,想了解测试 Android NDK 应用的方法可阅读第 9 章。 1.1 快速入门 Android 开发环境的安装,由于种种原因,变得非常难于实现,这在一定程度上给 Android 开发的初学者带来了很大的不便。为了帮助读者将主要精力放在学习 Android 自动 化测试的开发技术上,而不是将时间浪费在环境的准备和安装上,本书配套资源提供了一个 VirtualBox 虚拟机,方便读者学习和尝试本书里讲解的各种技术。本书后续章节的示例代码 和示例命令,如果没有特殊说明,均使用该虚拟机演示。使用该虚拟机可参照以下步骤: 1)下载并安装最新版本的 VirtualBox:https://www.virtualbox.org/wiki/Downloads。 2)打开 VirtualBox,依次单击菜单栏的“控制”和“注册”菜单项,导入“ AndroidBook. vbox”虚拟机。 3)在 VirtualBox 中选择刚刚注册的虚 拟机,单击工具栏里的“启动”按钮启动它。 4)在虚拟机上安装的是编写本书时最 新的 Ubuntu 12.04 操作系统,超级用户的用 户名和密码均为“student”。 5)在虚拟机中已经准备好了 Eclipse、 Android 开发环境和使用真机设备的相关配 置。在虚拟机中单击“ Dash”图标(或者同 时按下键盘的 Alt 和 F2),并输入“ gnome- terminal”打开终端程序,如图 1-1 所示。 图 1-1 在附带虚拟机中打开终端程序 ◆ 第 1 章 Android 自动化测试初探2 6)在终端中输入下列命令启动 Eclipse: $ ~/eclipse/eclipse 注意 在 Windows 中,如果当前登录用户的用户名是中文,当在资源管理器中双击 VirtualBox 的 .msi 安装包尝试安装时,VirtualBox 安装程序会弹出“系统找不到指定的路径”的错误消 息框,如图 1-2 所示。 图 1-2 “系统找不到指定的路径”的错误消息框 这是 VirtualBox 安装程序的一个缺陷,因为 MSI 安装程序运行时会解压一些临时文件 到登录用户的临时文件夹中,而 VirtualBox 安装程序对包含中文的路径支持得不是很好, 就会弹出这样的对话框,所以这里建议使用英文名的管理员用户安装。 1.2 待测示例程序 本章示例所采用的待测程序是一个简单的 Android 应用,模拟数据库程序的增删改查功 能。程序的主界面是一个书籍列表界面,按作者名列出了每个作者的著作书名。在列表中单 击书名可以查看书籍的详细信息,在详细信息界面上单击“编辑”按钮可以编辑书籍的信 息,完成后单击“保存”按钮即可保存更改并返回到列表界面。列表界面上有“删除”和“添 加”按钮,向列表中添加一本新书籍的操作与“编辑”类似;在从列表中删除一本书时,需 要先单击“删除”按钮,然后再单击要删除的书籍。待测示例程序的主界面如图 1-3 所示。 待测程序的源代码可以在配套资源(或虚拟机的 /home/student/samplecode 中) 的 “ chapter1\cn.hzbook.android.test.chapter1”文件夹中找到,按照下面的步骤在 Eclipse 中导入该工程: 1)启动 Eclipse 。 2)依次单击 Eclipse 菜单栏中的“ File ”、“ Import…”菜单项。 3)在新弹出的“ Import”对话框中选择“ Existing Projects into Workspace”列表项, 然后单击“Next”按钮进入下一步,如图 1-4 所示。 1.2 待测示例程序 ◆ 3 图 1-3 待测示例程序的主界面截图 图 1-4 在 Eclipse 中导入待测示例应用工程 4) 在“ Import Projects ” 界 面 中 勾 选“ Select root directory ” 项, 并 单 击 旁 边 的 “ Browse…” 按 钮, 填 入 配 套 资 源 中 待 测 应 用 源 文 件 的 根 目 录。 完 成 后 应 该 可 以 在 “ Projects”列表框中看到要导入的工程名:“ cn.hzbook.android.test.chapter1”。勾选“ Copy projects into workspace”复选框,以便将应用的源代码复制到本地硬盘。最后单击“Finish” 完成导入操作。如图 1-5 所示。 5)完成导入后,用右键单击刚导入的工程文件,并依次选择“ Run As”,“ Android ◆ 第 1 章 Android 自动化测试初探4 Application”运行应用。如图 1-6 所示。 图 1-5 在 Eclipse 中导入工程向导中选择待测示例工程 图 1-6 在 Eclipse 中运行示例待测应用 6) 这 时 Eclipse 会 自 动 启 动 Android 模 拟 器 并 打 开 应 用。 此 时 打 开 Eclipse 下 方 的 “Console”窗口并选择“Android”下拉框,应该可以看到类似图 1-7 的输出。 1.2 待测示例程序 ◆ 5 图 1-7 在 Eclipse 中启动应用的过程输出 图 1-7 的输出中详细显示了 Eclipse 从启动模拟器到运行应用的完整过程。在测试过程 中,会经常用到输出内容来排查错误,下面结合注释解释输出内容: # 在这里 Eclipse 接受到启动 Android 应用的命令 [2012-11-09 11:21:37 - cn.hzbook.android.test.chapter1.MainActivity] Android Launch! #Eclipse 首先确定用来与模拟器进行通信(调试程序)的 adb 程序是否运行,如果没有运行就会启动它 #adb 程序的用法会在后面调试章节中讲解 [2012-11-09 11:21:37 - cn.hzbook.android.test.chapter1.MainActivity] adb is running normally. # 找到应用的主 Activity 并启动它 [2012-11-09 11:21:37 - cn.hzbook.android.test.chapter1.MainActivity] Performing cn.hzbook.android.test.chapter1.MainActivity activity launch # 自动选择最合适的模拟器,这里因为示例应用最低要求 Android 2.2 版本,而系统中正好有 # Android 2.2 版本的模拟器,所以直接做出最佳选择 [2012-11-09 11:21:37 - cn.hzbook.android.test.chapter1.MainActivity] Automatic Target Mode: launching new emulator with compatible AVD 'Android2' [2012-11-09 11:21:37 - cn.hzbook.android.test.chapter1.MainActivity] Launching a new emulator with Virtual Device 'Android2' # 这些错误与 OpenGL 有关,可以忽略它们 [2012-11-09 11:21:40 - Emulator] OpenGL Warning: Failed to connect to host. Make sure 3D acceleration is enabled for this VM. [2012-11-09 11:21:41 - Emulator] Failed to load libGL.so [2012-11-09 11:21:41 - Emulator] error libGL.so: cannot open shared object fi le: No such fi le or directory [2012-11-09 11:21:41 - Emulator] Failed to load libGL.so [2012-11-09 11:21:41 - Emulator] error libGL.so: cannot open shared object fi le: No such fi le or directory # 模拟器会自动将自身窗口调整到屏幕的最佳位置 [2012-11-09 11:21:41 - Emulator] emulator: emulator window was out of view and was recentered [2012-11-09 11:21:41 - Emulator] ◆ 第 1 章 Android 自动化测试初探6 # 在模拟器启动时,Eclipse 会不停扫描系统端口,一旦模拟器启动完毕,Eclipse 就会连接上它 [2012-11-09 11:21:42 - cn.hzbook.android.test.chapter1.MainActivity] New emulator found: emulator-5554 [2012-11-09 11:21:42 - cn.hzbook.android.test.chapter1.MainActivity] Waiting for HOME ('android.process.acore') to be launched... [2012-11-09 11:22:13 - cn.hzbook.android.test.chapter1.MainActivity] HOME is up on device 'emulator-5554' #Eclipse 将编译好的应用上传到模拟器 [2012-11-09 11:22:13 - cn.hzbook.android.test.chapter1.MainActivity] Uploading cn.hzbook.android.test.chapter1.MainActivity.apk onto device 'emulator-5554' # 接着安装应用 [2012-11-09 11:22:13 - cn.hzbook.android.test.chapter1.MainActivity] Installing cn.hzbook.android.test.chapter1.MainActivity.apk... [2012-11-09 11:22:35 - cn.hzbook.android.test.chapter1.MainActivity] Success! # 成功安装后,就直接启动应用 [2012-11-09 11:22:35 - cn.hzbook.android.test.chapter1.MainActivity] Starting activity cn.hzbook.android.test.chapter1.MainActivity on device emulator-5554 [2012-11-09 11:22:37 - cn.hzbook.android.test.chapter1.MainActivity] ActivityManager: Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category. LAUNCHER] cmp=cn.hzbook.android.test.chapter1/.MainActivity } 注意 模拟器启动后,默认处于锁屏状态,需要手动解锁,解锁后就会看到应用已经启动。自 动解锁的方式在 10.3 节的代码清单 10-10 中说明。 1.3 第一个 Android 应用测试工程 本章先建立一个简单的 Android 自动化单元测试工程来演示 Android 自动化测试的流 程。在 Android 系统中,Android 自动化单元测试也是一个 Android 应用工程,它跟普通 Android 应用工程不同的地方是启动的方式不一样,这在本书第 3 章会讲到。 Android 的 Eclipse ADT 插件提供了 Android 自动化单元测试的模板,方便我们创建自 动化测试项目,这里新建一个测试工程: 1)启动 Eclipse ,这次可以看到之前在工作空间已被导入的 Android 工程。 2)依次单击 Eclipse 菜单栏里的“File”、“New”、“Project…”菜单项,如图 1-8 所示。 图 1-8 在 Eclipse 中新建工程(1) 3)在弹出的“ New Project”对话框中,展开“ Android”列表项,并选择“ Android Test Project”项来指明要创建一个 Android 自动化测试工程,然后单击“ Next”按钮,如 1.3 第一个 Android 应用测试工程 ◆ 7 图 1-9 所示。 图 1-9 在 Eclipse 中新建 Android 工程(2) 4)在接下来的“ Create Android Project”对话框中,在“ Project Name”文本框中输入 工程名称,一般来说,自动化测试工程的名称是在待测应用的名称后加上 .test 后缀。这里 的待测应用是在 1.2 节导入的 cn.hzbook.android.test.chapter1,因此将测试工程命名为“ cn. hzbook.android.test.chapter1.test”。单击“Next”按钮进入下一步。如图 1-10 所示。 图 1-10 为第一个测试程序命名 ◆ 第 1 章 Android 自动化测试初探8 5)在“ New Android Test Project”对话框中,由于待测应用是另外一个工程,因此一 般建议将测试代码和产品代码分离,选中“ An existing Android project:”单选框,并在下面 的列表框中选择 1.2 节导入的 cn.hzbook.android.test.chapter1,选择“ Finish”按钮完成测试 工程的创建,如图 1-11 所示。 图 1-11 在新建测试工程向导中选择被测应用 6)这时会在 Eclipse 中展开刚刚创建的测试工程,可以看到已经自动创建了一个与工程 同名的空的 Java 包,如图 1-12 所示,我们将在这个包中添加测试代码。 图 1-12 测试工程的结构 7)在 Eclipse 中右键单击“ cn.hzbook.android.test.chapter1.test”包,依次选择“ New” 和“JUnit Test Case”菜单项来创建一个测试用例源文件,如图 1-13 所示。 8)在弹出的“ New JUnit Test Case”对话框中,有两种单元测试类型 JUnit 3 和 JUnit 4,分别对应 JUnit 不同版本测试用例的编写方式,这两种编写方式在 2.2 节中讲解,这里我 们选择“New JUnit 3 Test”单选框。 1.3 第一个 Android 应用测试工程 ◆ 9 图 1-13 新建测试用例源文件 除了“ Name”和“ Superclass”文本框以外,其他控件均使用默认值。在“ Name”文 本框中输入“ HelloWorldTest”,单击“ Superclass”文本框附近的“ Browse…”按钮,如 图 1-14 所示。 图 1-14 为新测试用例命名 ◆ 第 1 章 Android 自动化测试初探10 9)在弹出的“ Superclass Selection”对话框的“ Choose a type”文本框中输入“ Activity- InstrumentationTestCase2”,并单击“OK”按钮,指明新单元测试的基类是“ActivityInstrume- ntationTestCase2”,如图 1-15 所示。 图 1-15 选择测试用例的基类 10)单击“Finish”按钮添加测试用例。 11)这时新创建的测试用例源文件会有一个编译错误,这是因为 ActivityInstrumenta- tionTestCase2 是一个泛型类,我们没有为它指明实例化泛型的类型参数,Java 泛型将在第 2 章中介绍,这里我们暂时忽略这个编译错误,如图 1-16 所示,下一步会解决它。 图 1-16 新测试用例的源文件 12)在 HelloWorldTest.java 中用代码清单 1-1 的代码替换原来的代码并保存。 代码清单 1-1 Android 自动化测试代码的简明示例 1. package cn.hzbook.android.test.chapter1.test; 2. 1.3 第一个 Android 应用测试工程 ◆ 11 3. import cn.hzbook.android.test.chapter1.MainActivity; 4. import cn.hzbook.android.test.chapter1.R; 5. import android.test.ActivityInstrumentationTestCase2; 6. import android.widget.Button; 7. 8. public class HelloWorldTest extends 9. ActivityInstrumentationTestCase2 { 10. 11. public HelloWorldTest() { 12. super(MainActivity.class); 13. } 14. 15. @Override 16. protected void setUp() throws Exception { 17. super.setUp(); 18. } 19. 20. public void test 第一个测试用例 () throws Exception { 21. fi nal MainActivity a = getActivity(); 22. assertNotNull(a); 23. fi nal Button b = 24. (Button)a.fi ndViewById(R.id.btnAdd); 25. getActivity().runOnUiThread(new Runnable() { 26. public void run() { 27. b.performClick(); 28. } 29. }); 30. 31. Thread.sleep(5000); 32. } 33. } 13) 在 Eclipse 中用右键单击”cn.hzbook.android.test.chapter1.test”工程,依次单击“Run As”和“Android JUnit Test”菜单项,如图 1-17 所示。 图 1-17  在 Eclipse 中运行 Android 自动化测试用例 14)如果没有连接真机设备,Eclipse 会启动模拟器,并打开“ cn.hzbook.android.test. chapter1”应用,单击“添加”按钮,等待 5 秒钟,关闭应用。这时在 Eclipse 中会多出一个 ◆ 第 1 章 Android 自动化测试初探12 “JUnit”的标签,其中显示了测试结果——执行并通过一个测试用例,如图 1-18 所示。 在代码清单 1-1 中,有 JUnit 使用经验的读者可以发现代码 是一个非常标准的 JUnit 单元测试代码,其只有一个测试用例 “ test 第一个测试用例”,在 JUnit 3 中,以“ test”为前缀命名 的函数会被当作一个测试用例执行。在代码清单 1-1 的第 23 行, 测试用例首先获取对待测应用“添加”按钮的引用,它的标识符 是“ btnAdd”;第 24 到 28 行针对刚刚抓取到的按钮执行了一个 单击操作;因为自动化测试代码执行速度比人工操作要快很多, 所以我们在第 31 行加入了一个显式等待 5 秒钟的操作,等待待 测应用的界面更新,以便看到自动化的效果。 1.4 搭建自动化开发环境 在 1.2 和 1.3 节的讲解中我们看到,Eclipse 在调试运行 Android 应用和测试代码时,自 动启动 Android 模拟器,在模拟器上部署相关应用和代码并运行,这是因为演示用的虚拟机 已经搭建好了 Android 开发环境,如果需要在物理机或其他机器上搭建同样的环境,也可以 参照本节的做法。 1.4.1 安装 Eclipse 和 ADT 开发包 因为 Eclipse 和 Android SDK 开发环境均需要 JDK 支持,所以要先下载并安装最新的 JDK。 在 Windows 平台下,直接在 Oracle 的官网(http://www.oracle.com/technetwork/java/ ❏ javase/downloads/index.html)下载最新的 Java SE SDK 并安装即可。 在 Linux 平台下,可以安装从 Oracle 官网上下载的 SDK,也可以使用系统自带的软 ❏ 件包管理工具安装其他 JDK 实现,例如在 Ubuntu 12.04 上,可以使用下列命令安装 Java SDK: $ sudo apt-get install openjdk-7-jdk Android 还在官网上建议,如果机器上运行的 Ubuntu 是 64 位版本,在安装 JDK 之 ❏ 前,需要通过如下命令安装 ia32-libs 包: $ sudo apt-get install ia32-libs 接下来安装 Eclipse。Android SDK 仅支持 Eclipse 3.6 以上版本。无论是 Windows 还 是 Linux 平台,都推荐从 Eclipse 官网(http://www.eclipse.org/downloads/)下载 SDK,谷 歌建议使用 Eclipse Classic 4.2、Eclipse IDE for Java Developers 或 Eclipse for RCP and RAP Developers 版本。 最 后 安 装 Android SDK。Android 为 Eclipse IDE 提 供 了 一 个 插 件, 名 为 Android 图 1-18 cn.hzbook.android. test.chapter1.test 的运行结果 1.4 搭建自动化开发环境 ◆ 13 Development Tools(ADT),可以通过 Eclipse 下载并安装 ADT 及 Android SDK。 1)启动 Eclipse,在菜单中选择 Help-> Install New Software。 2)在新打开的对话框中单击右上角的“Add”按钮。 3) 在“ Add Repository ” 对 话 框 的“ Name ” 文 本 框 中 输 入“ ADT Plugin ”, 在 “Location”文本框中输入下列网址: https://dl-ssl.google.com/android/eclipse/ 或 http://dl-ssl.google.com/android/eclipse/ 4)单击“OK”按钮关闭“Add Repository”对话框,并得到 Eclipse 下载完的安装列表。 5)在“ Available Software”对话框中选择 “Developer Tools”复选框并单击“Next”按钮, 进入如图 1-19 所示的界面。 6)接下来显示的对话框会列出将要下载的 工具列表,单击“Next”按钮确认。 7)最后接受授权协议并单击“ Finish”按 钮开始安装。如果 Eclipse 弹出一个对话框警告 无法验证软件的身份和完整性,单击“OK”按钮。 8)最后重启 Eclipse 就可以使用 Android 开发环境了。 注意 有时会因为网速慢或网络上有防火墙导致安装失败,这时可以尝试离线安装,在本书 配套资源中附有本书写作时最新的 Android 开发插件包 20.0.3 版本,使用方法是在“ Add Repository”对话框的“ Name”文本框中输入“ ADT Plugin”,单击“ Archive…”按钮, 添加 ADT-20.0.3.zip,单击“OK”按钮完成压缩包安装,如图 1-20 所示。 图 1-20 安装 ADT 时指定插件仓库位置 1.4.2 创建模拟器 Android 自动化测试用例可以运行在模拟器和真机上。模拟器的执行效率比真机要低 图 1-19 安装 Android 开发工具包(ADT) ◆ 第 1 章 Android 自动化测试初探14 一些,建议在没有真机的情况使用模拟器开发和执行测试用例。使用 Android SDK 的工具 AVD Manager 创建模拟器,它提供了图形界面和命令行支持。 可以使用 AVD Manager 创建任意多个模拟器以便测试时使用。一般建议在要求的最低 版本之上的所有 Android 版本将应用都测试一遍,这时创建多个模拟器并在其上执行自动化 测试就不失为一个好策略。 在创建模拟器时,选择模拟器的 Android 系统需要注意以下几点: 模拟器使用的 Android 系统版本应该不低于待测应用要求的版本,待测应用要求的最 ❏ 低版本可以在其工程的 manifest.xml 文件的 minSdkVersion 中找到,否则待测应用无 法在模拟器上启动。 建议在测试待测应用时,除了在其要求的最低版本上执行测试,至少还应该在最新 ❏ 版本的 Android 系统模拟器上测试一遍,以验证待测应用的向后兼容性。向后兼容测 试确保在用户升级他的手机系统后,应用还能继续正常工作。 如果待测应用在 manifest 文件中通过声明 uses-library 元素来指明对外部库的依赖 ❏ 关系,那么应用只能在包含该外部依赖库的 Android 系统上运行,因此一般需要使 用包含了附加(Add-on)组件的 Android 系统来创建模拟器。 1.通过图形界面创建 通过以下方式启动 AVD Manager: 在 Eclipse 中, 要 么 是 依 次 单 击 ❏ 菜 单 里 的“ Window ”>“ AVD Manager”,要么是单击工具栏中 的“AVD Manager”图标。 如 果 使 用 的 是 其 他 IDE, 可 以 ❏ 切 换 到 Android SDK 的 主 目 录 的“ tools/”文件夹,双击或在终 端 上 执 行“~ /android-sdk/tools/ android avd”命令。 单击“New”按钮,会弹出“Create New AVD”对话框来创建一个新的模拟 器,如图 1-21 所示。 参考表 1-1 在图 1-21 中填入信息。 表 1-1 新模拟器的设置参数 属  性 值 说  明 Name Android22 新模拟器的名称,这个名称是系统唯一的,当在命令行中启动模 拟器时,使用 Name 属性的值指定模拟器 Target Android 2.2-API Level 8 模拟器中 Android 系统的版本号 图 1-21 创建模拟器 1.4 搭建自动化开发环境 ◆ 15 属  性 值 说  明 SD Card Size 256 虚拟的 SD 卡的空间,默认以 MB 计算,也可以通过旁边的下拉 框选择新的计量单位 单击“Create AVD”按钮完成创建模拟器。 在 Android 模拟器管理窗口上,选择刚刚创建好的模拟器,并单击“Start”按钮。 在新弹出的“ Launch Options”对话框上单击“ Launch”最终启动模拟器。“ Launch Options”对话框上的几个选项是用来控制模拟器启动的参数,如表 1-2 所示。 表 1-2 模拟器启动的参数说明 参  数 说  明 Scale display to real size 将模拟器缩放到与真实设备相同的尺寸,由于手机屏幕要比电脑屏幕的像素密度大, 因此,对于相同尺寸的屏幕,手机屏幕上显示的像素要比电脑屏幕多,例如同样是 800 × 600 的分辨率,手机屏的物理尺寸看起来要明显比电脑屏的物理尺寸小。 这个选项有三个子选项: Screen size (in),模拟器屏幕的物理尺寸,单位是英寸。 Monitor dpi,电脑屏幕的每英寸像素点数,默认值是 96,单击问号可以通过指定的电 脑尺寸和分辨率大小自动计算出该值。 Scale,表示模拟器屏幕和实际显示屏幕尺寸的比例,1 表示尺寸相同,小于 1 则表示 实际显示的模拟器屏幕将被缩小,反之则放大 Wipe user data 清除用户自定义数据,重置虚拟机。建议在大规模自动化测试时启用这个选项,可以 规避前面测试用例在模拟器中写入或修改了一些数据,影响到后面测试用例的正常执行 Launch from snapshot 使模拟器从现有的镜像中恢复。如果在创建模拟器时,勾选了“ Snapshot Enabled”选 项,那么这个选项默认是启用的。 这个选项和后面的“ Save to snapshot”选项在下面的“使用镜像功能加快模拟器的 启动速度”中会讲到 2.使用镜像功能加快模拟器的启动速度 在前面的演示中,也许会发现 Android 模拟器重新启动的速度很慢,因此在 ADT r9 版 之后,新增了一个保存和恢复模拟器状态的镜像功能,用以加快模拟器重启的速度。镜像功 能的原理是将整个模拟器进程中的内存保存到硬盘中,从镜像恢复的过程实际上是将原先保 存在硬盘中的内存文件恢复到模拟器进程的内存中。因此在镜像恢复过程中,由于跳过了模 拟器启动和初始化的步骤,使启动速度变得很快;然而在关闭模拟器的时候,又因为需要保 存内存到硬盘中,关闭模拟器进程的过程会比不使用镜像功能时长一点。Android 官方博客 上说,这个功能并没有得到非常完整的测试,之所以会发布,是因为已经足够日常使用了, 因此要小心使用该功能。 1)首先需要编辑模拟器配置以启用该功能,启动 AVD Manager,在模拟器列表中选择 要设置的模拟器,并单击“Edit”按钮编辑它。 2)在“ Edit Android Virtual Device (AVD)”对话框中找到“ Snapshot”一栏并勾选 “ Enabled”复选框,单击“ Edit AVD”按钮保存设置,这样就为模拟器启用了镜像功能, (续) ◆ 第 1 章 Android 自动化测试初探16 如图 1-22 所示。 图 1-22 编辑模拟器设置 3)在 AVD Manager 窗口上选择刚刚编辑过的模拟器并单击“ Start…”按钮,这时可以 看到“ Launch from snapshot”和“ Save to snapshot”两个选项默认已经勾选,分别代表从 镜像中恢复模拟器状态和将模拟器状态保存到镜像中,如图 1-23 所示。 4)这时再启动几次模拟器,就会发现启动速度比 原先快了不少,因为已经把启动过程省略了。 在启用镜像功能后,还要注意下面几点。 如果要重启模拟器,也就是执行模拟器的启动 ❏ 和初始化过程,需要在“ Launch Opitons”对话 框中勾掉“ Launch from snapshot”复选框,这 时再单击“ Launch”按钮就不是从镜像中恢复 模拟器了,而是从头开始启动模拟器。 从镜像恢复后,模拟器大概需要 8 秒左右的时 ❏ 间,将时间从原来镜像中的时间修正为当前时间。 有些模拟器可能不会在镜像中保存一些设置, ❏ 例如信号强度设置,因此在模拟器从镜像中恢 复后,信号强度就被设置为默认值了。 图 1-23 使用镜像功能快速启动模拟器 1.4 搭建自动化开发环境 ◆ 17 3.通过命令行创建 在大规模自动化测试中,通过图形界面一个个单独地去创建模拟器显然费时费力,因此 AVD Manager 也提供了命令行界面以便将创建模拟器的过程集成到自动化测试中,这样做 的另一个好处是,其他 IDE 也可以通过命令行创建模拟器。 在 Android 中,AVD Manager 的图形界面和命令行界面均由同一个程序 android 创建, 所不同的是,如果向 android 传递一个 avd 参数,如下: $ android avd 则会启动 AVD Manager 的图形界面,使用其他参数则通过命令行界面。在本节我们通 过命令行界面再创建一个与前面相同的模拟器。 1)在虚拟机中按 ALT + F2 组合键,并输入“gnome-terminal”打开命令行。 2)在创建模拟器之前,需要指明模拟器的 Android 系统版本,在 Android SDK 工具包 中,每个 Android 系统都被分配了一个标识号,这个标识号可以通过“ android list targets” 查看。在虚拟机中查看到的输出如代码清单 1-2 所示。 代码清单 1-2 列出机器上安装的可用 Android 系统版本的命令 student@android-student:~$ android list targets Available Android targets: ---------- id: 1 or "android-8" Name: Android 2.2 Type: Platform API level: 8 Revision: 3 Skins: HVGA, WQVGA432, WQVGA400, WVGA800 (default), QVGA, WVGA854 ABIs : armeabi ---------- ...... ---------- id: 4 or "Samsung Electronics Co., Ltd.:GALAXY Tab Addon:8" Name: GALAXY Tab Addon Type: Add-On Vendor: Samsung Electronics Co., Ltd. Revision: 1 Based on Android 2.2 (API level 8) Skins: WVGA854, WQVGA400, GALAXY Tab (default), HVGA, WQVGA432, QVGA, WVGA800 ABIs : armeabi ...... 在上面输出的第一部分中,id: 1 表示 Android 2.2 这个版本的标识号是 1,后面创建 Android 2.2 的模拟器时,需要用到这个值。Type: Platform 表明这是一个标准的 Android 版 本,没有外挂任何其他组件。而在输出的第二部中,Type 的值是 Add-On,表明这是一个其 他 Android 设备厂商定制的版本,附有一些额外的组件。 关于 target Id 这个值,需要说明的是,这个值与 Android 系统的版本号没有任何关系, ◆ 第 1 章 Android 自动化测试初探18 在不同的宿主系统下,相同版本的 Android 系统的 target Id 有可能是不一样的,因此在使用 命令行创建模拟器之前,一定要执行上面的命令确认指定的 Android 系统的 target Id。 3)在命令行中创建模拟器的命令格式是:android create avd-n < 模拟器名称 >-t < 目标 Android 系统标识号 >[-< 选项 >< 选项的值 >] …。与前面一样,创建一个 Android 2.2 系统 的模拟器,并把模拟器命名为“Android22”,代码如代码清单 1-3 所示。 代码清单 1-3 创建模拟器命令 student@android-student:~$ android create avd-n Android22-t 1 Auto-selecting single ABI armeabi Android 2.2 is a basic Android platform. Do you wish to create a custom hardware profi le [no] 4)如果选择的目标 Android 系统是一个标准 Android 系统,即步骤 2)中输出类型为 “ Type: platform”的系统,那么下一步 Android 工具会询问硬件配置情况。如果需要定制一 些硬件配置,那么输入“ yes”并设置相应的值,这里使的是用默认的硬件配置,因此直接 按回车,默认选择选项“no”。 Do you wish to create a custom hardware profi le [no]no Created AVD 'Android22' based on Android 2.2, ARM (armeabi) processor, with the following hardware confi g: hw.lcd.density=240 vm.heapSize=24 5)稍等片刻,一个新的模拟器就创建好了,可以用 android 程序列出当前系统中已经 创建的模拟器: $ ~/android-sdk/tools/android list avd android 程序会扫描当前登录用户目录的 .android 隐藏文件夹,列出其 avd 目录中的所 有现有模拟器,针对每个模拟器打印类似代码清单 1-4 的输出。 代码清单 1-4 打印模拟器列表命令 student@android-student:~$ android list avd Available Android Virtual Devices: Name: Android22 Path: /home/student/.android/avd/Android22.avd Target: Android 2.2 (API level 8) ABI: armeabi Skin: WVGA800 上面的输出打印了模拟器的一些基本设置,例如模拟器在宿主机上的文件位置,模拟 器使用的 Android 系统版本号,是否启用了镜像功能(没有启用镜像则不会显示镜像的设 置)等。 android 命令在宿主机上创建一个专用的文件夹来存放模拟器的信息,包括模拟器的配 1.4 搭建自动化开发环境 ◆ 19 置文件、用户数据以及虚拟 SD 卡等。这个文件夹并不包含模拟器使用的 Android 系统文 件,而是通过在配置文件中指明目标系统标示号,这样模拟器启动时会自动从 Android 开发 工具包中加载系统镜像。 android 命令还在目录 .android/avd/ 下为新的模拟器创建以模拟器名称命名的 .ini 文件, 在上例中文件名是 Android22.ini,该文件指明了模拟器配置文件的保存地址。 在 Linux 或 Mac 系 统 中, 模 拟 器 的 配 置 文 件 夹 默 认 放 在 ~ /.android/avd/ 中, 在 Windows XP 系 统 中, 默 认 存 放 在 C:\Documents and Settings\\.android\ 中, 而 在 Windows Vista/7 中放在 C:\Users\\.android\ 下。如果要指定一个不同的路径,可以在 创建模拟器的命令后加上 -p < 路径 > 参数,例如: $ android create avd-n my_android1.5 -t 2 -p 其他路径 一个 Android 虚拟设备——AVD(Android Virtual Device)是一个包含了真机设备的 硬件和软件配置信息的模拟器,在本书中,简称它为模拟器。既可以通过图形化的 AVD Manager 来创建和启动模拟器,也可以在命令行中创建、管理和启动模拟器。无论是通过图 形还是命令行界面,在一台宿主机上可以同时创建和启动任意数量的模拟器。 一个 AVD 由下面这些部分构成: 硬件配置,定义了要模拟的硬件功能。例如,可以指定是否配有相机,是否配有物 ❏ 理键盘,多大内存等选项。 软件配置,定义了模拟器上运行的 Android 平台的版本,既可以指定标准的 Android ❏ 版本,也可以是定制的 Android 系统。 外观配置,可以定义模拟器使用的皮肤,通过皮肤控制模拟器的屏幕物理尺寸、外 ❏ 观,还可以指定模拟器使用的虚拟 SD 卡。 在宿主机上的存储区域,模拟器上的用户数据(例如已安装的程序,个人设置等数 ❏ 据)和虚拟 SD 卡都存储在这个地方。 以刚刚创建的模拟器“Android22”为例,其在虚拟机中的配置文件路径是: student@student:~$ ls .android/avd/Android2.avd/ cache.img hardware-qemu.ini userdata-qemu.img cache.img.lock hardware-qemu.ini.lock userdata-qemu.img.lock confi g.ini userdata.img 其中各个文件的作用如表 1-3 所示。 表 1-3 Android 模拟器文件夹中各文件的说明 文 件 名 说  明 hardware-qemu.ini 硬件配置文件,可以通过下面的命令查看其内容: $ gedit .android/avd/Android2.avd/hardware-qemu.ini userdata-qemu.img 用来存放用户数据,可读写,Android 启动后将其挂载到 /data 文件夹上 userdata.img 一般不使用 userdata.img,只有在使用 -wipe-data 参数启动模拟器的时候才会用 userdata.img 的内容覆盖 userdata-qemu.img ◆ 第 1 章 Android 自动化测试初探20 文 件 名 说  明 cache.img Android 启动后会将其挂载到 /cache 文件夹上,/cache 文件夹或者说分区是 Android 用来保 存经常访问的应用组件和数据的,系统每次重启时都会重建这个分区 *.lock 这些文件都是临时文件,只有当模拟器启动时才会创建,模拟器关闭后就自动删除,它们的 目的系统用来防止在模拟器运行时,用户不小心通过模拟器管理器修改模拟器设置的 confi g.ini 保存软件配置和外观配置文件,可以通过下面的命令查看其内容: $ gedit .android/avd/Android2.avd/confi g.ini 可以手工编辑或通过模拟器管理器来修改模拟器的设置,具体方法在 1.4.3 中讲述 可以将新建好的模拟器移动到其他目录,由 -n 选项指明要移动的模拟器名称,由 -p 选项指定移动后的目录,这个目录事先不能存在,由 android 创建。下面的示例就是将 名为 Android22 的模拟器配置文件夹移动到 /tmp/Android22 文件夹中,如代码清单 1-5 所示。 代码清单 1-5 移动模拟器命令 student@android-student:~$ android move avd -n Android22 -p/tmp/Android22 AVD 'Android22' moved. student@android-student:~$ android list avd Available Android Virtual Devices: Name: Android22 Path: /tmp/Android22 Target: Android 2.2 (API level 8) ABI: armeabi Skin: WVGA800 也可以重命名模拟器,下面的命令将名为 Android22 的模拟器重命名为 Android2,模拟 器新的名称由 -r 选项指定,如代码清单 1-6 所示。 代码清单 1-6 重命名模拟器的命令 student@android-student:~$ android move avd -n Android22 -r Android2 AVD 'Android22' moved. student@android-student:~$ android list avd Available Android Virtual Devices: Name: Android2 Path: /tmp/Android22 Target: Android 2.2 (API level 8) ABI: armeabi Skin: WVGA800 执行下面的命令删除刚刚创建的模拟器,这样会将模拟器的配置文件、用户数据及虚拟 SD 卡等数据从硬盘上删除,如代码清单 1-7 所示。 (续) 1.4 搭建自动化开发环境 ◆ 21 代码清单 1-7 删除模拟器的命令 student@android-student:~$ android delete avd -n Android2 Deleting fi le /home/student/.android/avd/Android2.ini Deleting folder /tmp/Android22 AVD 'Android2' deleted. student@android-student:~$ android list avd Available Android Virtual Devices: Android SDK 中的 android 命令有很多用处,本书不能一一介绍,有兴趣的读者可以通 过“-h”参数查看其用法和子命令,如代码清单 1-8 所示。 代码清单 1-8 查看 android 命令的用法 student@student:~$ android-sdks/tools/android -h Usage: android [global options] action [action options] Global options: -h --help : Help on a specifi c command. ...... -create avd : Creates a new Android Virtual Device. ...... 而使用“android –h < 子命令 >”可查看各个子命令的使用方法,如代码清单 1-9 所示。 代码清单 1-9 查看 android 子命令的用法 student@student:~$ android-sdks/tools/android -h create avd ...... Options: ...... -n --name : Name of the new AVD. [required] ...... 1.4.3 启动模拟器 在创建好模拟器之后,可以使用 Android SDK 中的 emulator 命令启动模拟器。emulator 命令只需要知道模拟器名称,通过“ -avd”参数指定要启动的模拟器名称就可以将其启动 它。通过下面的命令可以启动上一节创建的模拟器: $ emulator –avd Android2& 在默认情况下,分配给模拟器的内存只有 128MB,这个内存在大部分情形下都显得太 小了。有几个方法可以修改模拟器的内存大小。 1)通过 emulator 的“ -memory”参数指定模拟器内存的大小(按 MB 计算)。这种修改 方式只影响本次启动的模拟器,在后续启动时,如果不指定“ -memory”参数,还是采用模 ◆ 第 1 章 Android 自动化测试初探22 拟器自身的设置: $ emulator –avd Android2 –memory 512 & 在模拟器启动后,用代码清单 1-10 的命令验证内存大小。 代码清单 1-10 查看模拟器内存大小的命令 student@student:~$ adb -e shell cat /proc/meminfo MemTotal: 516452 kB MemFree: 394312 kB Buffers: 0 kB ...... VmallocChunk: 432132 kB 2)通过模拟器管理器修改“ device ram size”参数,如图 1-24 所示。启动模拟器管理 器的方法可参看 1.4.2 小节中“通过图形界面创建”的内容,这个做法是修改了模拟器自身 的设置,因此以后启动模拟器时都会采用这个内存设置。 3)修改模拟器配置文件夹的 confi g.ini 文件。在 1.4.2 小节的“通过命令行创建”中提 到,android 在创建模拟器时实际上是将模拟器的设置保存在模拟器配置文件夹下,其中有 一个 confi g.ini 文件,在其中添加或修改 hw.ramSize 参数即可改变模拟器的内存大小。这种 做法和上一步使用图形界面的方法的效果是相同的,如图 1-25 所示。 图 1-24 通过模拟器管理器修改模拟器内存大小 图 1-25 修改 confi g.ini 文件中内存大小的设置 除了修改内存大小外,还可以指定其他硬件参数,如表 1-4 所示。 表 1-4 创建模拟器可用的硬件参数 参  数 说  明 hw.ramSize 模拟器的物理内存大小,按兆字节计算,默认大小是“96” hw.touchScreen 是否支持触摸屏,默认值是“yes” hw.trackBall 是否有轨迹球(trackball),默认值是“yes” hw.keyboard 是否有 QWERTY 柯蒂键盘,默认值是“yes” hw.dPad 是否有 DPad 键(方向键),默认值是“yes” hw.gsmModem 是否有 GSM 调制解调器,默认值是“yes” hw.camera 是否有照相机设备,默认值是“no” hw.camera.maxHorizontalPixels 照相机的最大水平像素值,默认值是“640” hw.camera.maxVerticalPixels 照相机的最大垂直像素值,默认值是“480” 1.4 搭建自动化开发环境 ◆ 23 参  数 说  明 hw.gps 是否有 GPS 仪,默认值是“yes” hw.battery 是否需要电池,默认值是“yes” hw.accelerometer 是否有重力加速仪,默认值是“yes” hw.audioInput 是否支持录制音频,默认值是“yes” hw.audioOutput 是否支持播放音频,默认值是“yes” hw.sdCard 是否支持插入和移出虚拟 SD 卡,默认值是“yes” disk.cachePartition 模拟器上是否使用 /cache 分区,默认值是“yes” disk.cachePartition.size 缓存区的大小,默认值是“66MB” hw.lcd.density 模拟器屏幕的密度,默认值是“160” 注意 在本书配套资源附带的虚拟机中,在模拟器启动后,可能会发生无法关闭模拟器的现 象,如果有类似问题,可参照下面的步骤杀死模拟器。 在终端上使用“ ps aux | grep emulator”列出模拟器进程,并用“ kill-9 < 进程 ID>”杀 死模拟器进程,例如杀死模拟器进程 ID 是“3333”的模拟器进程的过程如下: student@student:~$ ps aux | grep emulator student   3333 33.8 9.3 352112 193604 pts/0 Sl+ 18:31 2:05 /home/student/ android-sdks/tools//emulator-arm-avd Android2 student 3374 0.0 0.0 5808 840 pts/2 S+ 18:37 0:00 grep-- color=auto emulator student@student:~$ kill -9 3333 1.4.4 连接模拟器 在模拟器启动后,可以随时按需要修改正在运行中的模拟器设置。在模拟器启动之后, 打开了一个网络套接字(socket)端口与宿主机通信,一些 Android 开发工具包中的工具就 是通过这个端口与模拟器交互的,我们也可以通过 telnet 程序操控模拟器。 在宿主机上可以同时启动多个模拟器,每个模拟器都会新开一个端口来与宿主机上的开 发工具通信,这个端口号显示在模拟器进程的标题栏上,如图 1-26 所示,这个模拟器和宿 主机通信的端口号是 5554。 在宿主机上打开一个终端窗口,并执行下面命令连接到模拟 器的控制端口: $ telnet localhost 5554 在连接成功后,模拟器会回显一些信息,用以提示操控命令,如代码清单 1-11 所示。 代码清单 1-11 使用 telnet 连接控制模拟器的命令 student@ubuntu:~$ telnet localhost 5554 图 1-26 模拟器的端口号 (续) ◆ 第 1 章 Android 自动化测试初探24 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Android Console: type 'help' for a list of commands 输入“help”命令显示所有可用的命令: help Android console command help: help|h|? print a list of commands event simulate hardware events geo Geo-location commands gsm GSM related commands cdma CDMA related commands kill kill the emulator instance network manage network settings power power related commands quit|exit quit control session redir manage port redirections sms SMS related commands avd control virtual device execution window manage emulator window qemu QEMU-specifi c commands sensor manage emulator sensors 要查看某个命令的详细帮助,可以执行“help < 命令名 >”查看。 例如,要动态修改正在运行的模拟器的大小比例,可以执行下面命令将模拟器尺寸缩小 到原来的四分之三: window scale 0.75 最后输入“quit”或“exit”命令退出模拟器控制台: quit Connection closed by foreign host. 1.4.5 连接手机 开发 Android 应用很重要的一点就是要在真机上实际测试,一方面有些功能在模拟器上 是无法测试的,必须在真机上测试,例如重力加速器;另一方面有些多手指手势的测试也很 难在模拟器上实施。而且 Android 系统的模拟器与 iOS 系统的模拟器不同,Android 模拟器 的运行速度远远慢于真机,因此必须准备一台测试用的真机。在真机上开发和调试 Android 应用的方法和模拟器上是一致的,设置真机开发环境需要执行下面这些步骤: (1)在应用的 Manifest 文件中声明应用是可调试的 (2)打开应用的调试支持 1.4 搭建自动化开发环境 ◆ 25 对于通过 Eclipse 创建的应用,可以省略这一步,在 Eclipse IDE 中启动应用时,会自动 打开应用的调试支持。 在 AndroidManifest.xml 文件的 元素中,添加 android:debuggable=“ true” 这个属性就为应用打开调试支持。代码清单 1-12 是一个启用调试支持的 AndroidManifest. xml 示例。 代码清单 1-12 在 AndroidManifest.xml 中启用真机调试 ...... 注意 在应用开发过程中,要在 manifest 文件中手动启用调试支持,最好在应用发布前关闭调 试支持,因为一个已发布的应用是不应该可以被调试的。 ◆ 第 1 章 Android 自动化测试初探26 (3)打开手机的“USB 调试”功能 在 Android 4.0 之前的设备上,依次选择“设置”、“应用程序”、“开发”,然后勾选“USB 调试”;在 Android 4.0 的设备上,“ USB 调试”选项位于“设 置 -> 开发”子菜单项中,如图 1-27 所示。 (4)设置宿主机系统以侦测到开发设备 如果是在 Windows 宿主机上进行 Android 应用开发,需要 为 adb 程序安装一个 USB 驱动,具体方法如下: 1)用 USB 数据线将设备与 Windows 机器连接,第一次连 接时提示发现新硬件,这时 Windows 弹出尝试为设备安装驱动 程序的对话框,选择“从列表或指定位置安装(高级)”单选框并单击“下一步”按钮,如 图 1-28 所示。 2)在接下来的对话框中选择“在这些位置上搜索最佳驱动程序”单选框,勾选“在搜 索中包括这个位置”复选框并输入 Android SDK 中的 usb_driver 文件夹的路径,这里输入 的是“C:\eclipse-java-juno-win32\android-sdk\extras\google\usb_driver”,然后单击“下一步” 按钮搜索并安装驱动,如图 1-29 所示。 图 1-28 在 Windows 上安装 Android 设备驱动程序 图 1-29 选择 Android 驱动文件路径 3)可能会出现好几个需要安装驱动的地方,要一个不少地全部安装成功。在搜索的时 候可能会提示需要某个文件,也不能跳过,一般来说可以在 Windows 安装目录的“system32\ drivers”(如“C:\WINDOWS\system32\drivers”)文件夹中找到相应的文件。 4)在驱动安装完毕后,可以使用设备管理器或 adb devices 命令查看系统是否成功识别 设备,如图 1-30 所示。 图 1-30 在 Windows 上验证 Android 设备驱动正确安装 图 1-27 启用手机 USB 调试 1.4 搭建自动化开发环境 ◆ 27 注意 如果在 Android SDK 的安装目录下没有找到 usb_driver 文件夹,即 android-sdk\extras\ google\usb_driver 不存在,这说明在安装 Android SDK 时,没有安装 usb_driver 这个包。 需 要打开 Android SDK Manager,找到“ Extras” 并勾选“ Google USB Driver”复选框,将其 安装,如图 1-31 所示。但是 Google 的驱动包并不支持有些设备,例如,笔者的三星手机, 需要下载三星的官方驱动包才能使用 Google 的驱动包。 图 1-31 安装 Google USB 驱动 如果是 Mac OS X 宿主机,可以即插即用,省略该步骤。 如果是 Ubuntu Linux 宿主机,需要为开发用的设备类型添加一个包含 USB 设置的 udev 规则文件。每个设备厂商都有一个唯一的供应商 ID(vendor ID)标识,这个标识通过在规 则文件中设置 ATTR{idVendor} 属性指定。以三星手机为例,在 Ubuntu Linux(配套资源中 的虚拟机)上设置设备即插即用侦测的方法如下: 1)将手机通过 USB 连接到电脑。 2)依次单击 VirtualBox 菜单栏中的“设备”、“分配 USB 设备”,并勾选连接到电脑上 的手机,如图 1-32 所示。 图 1-32 将 Android 设备连接到虚拟机 ◆ 第 1 章 Android 自动化测试初探28 3)以 root 的身份编辑文件 /etc/udev/rules.d/51-android.rules,在新装系统中,默认是没 有这个文件的,需要先创建它。使用下面的命令创建并编辑 51-android.rules 文件。 $ sudo gedit /etc/udev/rules.d/51-android.rules 4)在 51-android.rules 文件中,为每个厂商的设备添加格式如下的一行规则: SUBSYSTEM=="usb", ATTR{idVendor}=="04e8", MODE="0666", GROUP="plugdev" 其 中, 供 应 商 ID“04e8” 指 明 了 是 三 星 设 备;MODE 的 值 表 明 具 有 读 / 写 权 限, MODE 值的设置和 Linux chmod 命令类似;而 GROUP 定义了设备节点的所有人用户组。 5)然后执行命令启用规则: $ sudo chmod a+r /etc/udev/rules.d/51-android.rules 6)将手机连接到 PC 的 USB 端口,执行命令 adb devices 验证设置是否正确: student@android-student:~$ adb devices * daemon not running. starting it now on port 5037 * * daemon started successfully * List of devices attached i50906d210fe0 device 如果设置正确,adb devices 会列出连到电脑上的设备的设备号(上面代码中加粗的部分)。 设置好设备以后,在 Eclipse 中使用设备运行和调试应用的方式和模拟器完全一样,如 果电脑上同时连接多个设备,或者连接设备的同时还运行有模拟器,Eclipse 会弹出一个 “ Device Chooser”对话框,其中列出可用的模拟器和设备,选择需要的设备或模拟器来运 行和调试应用,如图 1-33 所示。 图 1-33 Android 设备选择工具 Android 官方网站(http://developer.android.com/tools/device.html#VendorIds)上有最新 1.5 本章小结 ◆ 29 的完整的供应商 ID 列表,在本书写作时,完整列表如表 1-5 所示。 表 1-5 Android 设备的供应商 ID 列表 设备厂商(Company) USB 供应商 ID (USB Vendor ID) 设备厂商(Company) USB 供应商 ID (USB Vendor ID) Acer(宏基) 502 NEC 409 ASUS(华硕) 0b05 Nook 2080 Dell(戴尔) 413c Nvidia 955 Foxconn 489 OTGV 2257 Fujitsu 04c5 Pantech 10a9 Fujitsu Toshiba 04c5 Pegatron 1d4d Garmin-Asus 091e Philips(飞利浦) 471 Google(谷歌) 18d1 PMC-Sierra 04da Hisense 109b Qualcomm 05c6 HTC 0bb4 SK Telesys 1f53 Huawei(华为) 12d1 Samsung(三星) 4E+08 K-Touch 24000 Sharp 04dd KT Tech 2116 Sony(索尼) 054c Kyocera 482 Sony Ericsson(索尼爱立信) 0fce Lenovo(联想) 17ef Teleepoch 2340 LG 1004 Toshiba(东芝) 930 Motorola(摩托罗拉) 22b8 ZTE(中兴) 19d2 1.5 本章小结 这一章,我们讲解了设置 Android 开发环境、启动模拟器,以及准备开发设备等自动 化开发的必要步骤,实际上这些步骤与开发一个正常的 Android 应用的准备步骤是完全 一样的。在第3章,我们将看到,在 Android 系统上,Android 自动化测试几乎就是一个 Android 应用,因此开发方法也很类似。不过 Android 自动化测试通过复用 JUnit 技术,极 大地缩短了测试人员的学习时间。接下来就来了解使用了 JUnit 编写自动化测试。 第 2 章 Android 自动化测试基础 本章将讲解编写 Android 自动化测试用例的基础知识,覆盖了编写测试代码所需的必备 Java 编程知识,Android 系统的基本工作方式,以及使用 JUnit 编写单元测试用例的基本知 识。对于具备这些知识的读者,可以跳过本章直接进入第 3 章开始学习。 2.1 Java 编程基础 本书大部分示例代码使用 Java 编程语言编写,因此要求读者具备一些 Java 编程知识, 本节简要讲解编写测试代码的一些必要知识。如果读者希望对 Java 编程语言有一个完整的 理解,建议参看《Java 语言程序设计:基础篇》(机械工业出版社 2011 年 11 月出版)。 Java 是一门面向对象的编程语言,其设计初衷是同一份代码可以移植并运行在几乎所有 硬件平台上。为了达到这个目标,Java 源程序在编译完毕后不像 C/C++ 源程序那样被编译 成目标机器上的可执行代码,而是编译成一个中间代码,由 Java 虚拟机在不同的硬件平台 上解释执行。 Java 是一门静态强类型语言。强类型是指,当声明一个变量的类型为整型时,在这个 变量的整个生命周期里,它都是整型的,不能将一个字符串或布尔值赋值给它,这一点跟 JavaScript、Python、Ruby 等动态语言不同。静态的意思是,当 Java 源程序编译时,Java 编 译器会执行代码检查,避免代码有违犯强类型理念的地方。与其他编程语言类似,Java 语言 也有字符串( String)、整型(int)、浮点数型(double)、布尔型(boolean)和数组型等数据 类型,也支持多种操作符。深入讨论各种类型的使用方法不在本书的范围之内,参考前面推 荐的书籍。 Java 程序的内部结构如图 2-1 所示。 通常,一个 Java 程序(包括测试用例程序)都包含零到多个包,每个包中包含许多 Java 类型,每个 Java 类型又有多个函数,而函数中包含实际执行计算操作的语句。虽然在处理 器层面执行的都是一条条 Java 语句,但在大多数编程语言中,函数是最小的可执行单位, 2.1 Java 编程基础 ◆ 31 这主要是为了体现代码复用的思路。比如,在一个外汇交易系统中,与其在系统中到处复 制、粘贴类似代码清单 2-1 所示的美元兑换人民币的计算代码: Java 包 Java 程序 Java 类 Java 包 函数 函数 类 类 类 语句 图 2-1 Java 程序结构示意图 代码清单 2-1 没有复用代码的汇率转换函数 1. double dollar = ...; // 设置要转换的美元金额 2. double rmb = dollar * 6.25; 3. // 使用转换后的人民币金额 rmb 执行一些计算 不如将美元兑人民币的汇率转换代码封装成一个函数,如代码清单 2-2 所示。 代码清单 2-2 将美元兑人民币的汇率封装成函数 1. // dollar 的值是要转换的美元金额 2. public double toRmb(double dollar) 3. { 4. return dollar * 6.25; 5. } 6. 7. double rmb = toRmb(100);// 兑换 100 美元 代码封装的好处显而易见。在代码清单 2-1 中第 2 行将汇率固定为 6.25,而实际上汇率 是在不停变换的。汇率一旦变化,就需要修改系统中所有类似代码 2-1 中第 2 行的代码。而 代码 2-2 中的封装方案使我们只需要改动一个地方就可以适应汇率的变化情况。 代码清单 2-2 还有很多的改进空间,例如在第 4 行硬编码了汇率,每次汇率变化都需要 修改代码,重新编译程序,可以将汇率封装成一个函数,从外部服务自动获取最新汇率,如 代码清单 2-3 所示。 ◆ 第 2 章 Android 自动化测试基础32 代码清单 2-3 自动获取汇率的函数 1. // dollar 的值是要转换的美元金额 2. public double toRmb(double dollar) 3. { 4. // fetchDollarExchangeRate 是一个函数 5. // 自动从某个外部服务抓取最新的汇率 6. return dollar * fetchDollarExchangeRate(); 7. } 8. 9. doublermb = toRmb(100); 在 Java 这样的面向对象的编程语言中,对代码封装的理念进行了进一步的扩展,使用 面向对象的编程理念将相关的函数放在一个类型中。例如前面汇率转换的例子,可以将各国 货币兑换的函数都放在一个类型中,方便其他程序员查找和使用。 代码清单 2-4 使用面向对象的理念封装汇率转换代码 1. public class Bank 2. { 3. // dollar 的值是要转换的美元金额 4. public double dollarToRmb(double dollar) 5. { 6. // fetchDollarExchangeRate 是一个函数 7. // 自动从某个外部服务抓取最新的汇率 8. return dollar * ExternalService.fetchDollarExchangeRate(); 9. } 10. 11. // dollar 的值是要转换的日元金额 12. public double YenToRmb(double yen) 13. { 14. // fetchDollarExchangeRate 是一个函数 15. // 自动从某个外部服务抓取最新的汇率 16. return yen * ExternalService.fetchYenExchangeRate(); 17. } 18. 19. ... 20. } 21. 22. public class Customer 23. { 24. public static void main(String[] args) 25. { 26. Bank bank = new Bank(); 27. // 兑换 100 美元 28. doublermb = bank.dollarToRmb(100); 29. // 兑换 100 日元 30. rmb += bank.YenToRmb(100); 31. } 32. } 2.1 Java 编程基础 ◆ 33 在上面的代码中,将所有货币兑换的函数都统一放在一个名为“ Bank”的类型中(第 1 到 20 行),这样调用者(Customer 类型——第 22 行)在兑换货币时,只要访问“ Bank” 找到对应的兑换函数就可以了(第 27 到 30 行)。这种封装方式与现实生活的场景很像,可 以将类想象成一个具体的实体,在此示例中 Bank 就是银行,而每个货币兑换函数就是 银行的兑换窗口,例如第 28 行代码的意思是,在兑换美元窗口(dollarToRmb)处,客户 “Customer”递给窗口 100 美元,然后得到窗口返回的人民币。 虽然在图 2-1 中显示类型还有可能包含在 Java 包中,但是类型就是代码封装的最高 层次了。Java 包的作用是避免类型重名的问题,例如在代码清单 2-4 中定义并使用了名 为“ Bank”的类型,但是在同一个程序内,可能会同时用到来自两个不同组织创建的同名 “ Bank”类型,为了防止混淆,就需要使用 Java 包来辅助区分。例如为一个跨国旅游团服 务的货币兑换系统的编码可能如代码清单 2-5 所示。 代码清单 2-5 使用包区分来自不同组织的类名重复问题 China/Bank.java 1. package China; 2. 3. public class Bank { 4. public double dollarToRmb(double dollar) { 5. return dollar * 6.25; 6. } 7. 8. public double rmbToDollar(double rmb) { 9. return rmb / 6.25; 10. } 11. 12. public double borrow(double someMoney) { 13. return someMoney * 0.9; 14. } 15. } USA/Bank.java 1. package USA; 2. 3. public class Bank { 4. public double dollarToRmb(double dollar) { 5. return dollar * 6.25; 6. } 7. 8. public double rmbToDollar(double rmb) { 9. returnrmb / 6.25; 10. } 11. 12. public double borrow(double someMoney) throws FinancialCrisisException { 13. throw new FinancialCrisisException(" 没钱啦 !"); 14. } 15. } ◆ 第 2 章 Android 自动化测试基础34 Customer.java 1. import USA.FinancialCrisisException; 2. 3. public class Customer extends Exception { 4. public static void main(String[] args) throws FinancialCrisisException { 5. arriveChina(); 6. China.Bank chinaBank = new China.Bank(); 7. double rmb = chinaBank.dollarToRmb(100); 8. // 省略无关代码 9. rmb = rmb + chinaBank.borrow(100); 10. leaveChina(); 11. 12. arriveUSA(); 13. USA.Bank usaBank = new USA.Bank(); 14. double dollar = usaBank.rmbToDollar(rmb); 15. // 省略无关代码 16. dollar = dollar + usaBank.borrow(100); 17. leaveUSA(); 18. } 19. 20. private static void leaveUSA() { 21. System.out.println("Left USA!"); 22. } 23. 24. private static void arriveUSA() { 25. System.out.println("Hello, USA!"); 26. } 27. 28. private static void leaveChina() { 29. System.out.println("Left China!"); 30. } 31. 32. private static void arriveChina() { 33. System.out.println("Hello, China!"); 34. } 35. } 代码清单 2-5 中的程序由 3 个源文件组成,在 Customer.java 中模拟了一位游客游览中 国和美国的货币兑换情形,其中引用了两个同名类:Bank。为了区分中国的银行和美国的 银行,将它们分别放在 China 和 USA 这两个 Java 包中,在使用时,在 Customer.java 文件 的第 6 和 13 行用包名加类名的方式限定使用的类型名,避免混淆。 另外,在 USA/Bank.java 文件的第 13 行,演示了 Java 报告错误的方式,通过抛出一个 异常中断正常的程序执行顺序,因此在 Customer.java 文件的第 16 行调用了 usaBank.borrow 函数时程序就会退出,并且不会执行第 17 行的语句,如下面是运行结果——注意没有打印 出“Left USA!”这条消息。 2.1 Java 编程基础 ◆ 35 Hello, China! Left China! Hello, USA! Exception in thread "main" USA.FinancialCrisisException: 没钱啦 ! at USA.Bank.borrow(Bank.java:13) at Customer.main(Customer.java:16) 总结前面的示例程序,Java 由以下几个编程元素组成。 (1)包 包名由英文字母、数字、下划线和点号组成。一般来说,Java 中每个包在文件系统中都 有一个文件夹与其对应,如果包名包含点号,则会创建层级文件夹对应包名。 例如在 Eclipse 中创建一个名为 China 的包,则会同时在文件系统中创建一个名为 China 的文件夹,而包名 China.Beijing 则对应 Windows 文件系统中的 China\Beijing,Linux 文件系统中的 China/Beijing。 当要在程序里使用一个包中的类型时,可以先用 import 子句导入包,再使用其中的类 型。如下语句导入了 USA 包中的所有类型。 import USA.*; (2)类 类这个概念是面向对象编程的核心理念,基本上,在 Java 源程序中,所有代码都被封 装在一个类中。如果一个类被标示为 public,那么它必须保存在与它同名的 .java 文件中, 而没有被标示为 public 的类可以保存在任意的 java 源文件中。例如一个标示为 public 的名 为 Bank 的类,一定要保存在 Bank.java 文件中——由于在 Linux/UNIX 文件系统中,文件名 是大小写敏感的,因此文件名的大小写也应与类名完全一致。 (3)函数 几乎所有编程语言都提供了函数的概念。函数是一个由多行代码组成,可以被程序其他 部分调用的代码块,用以提供良好的代码封装和复用的功能。代码清单 2-6 是 Java 语言中 函数的一个示例。 代码清单 2-6 Java 函数定义示例 1. public int Add(int left, int right) 2. { 3. return left + right; 4. } (4)语句 Java 语言跟其他编程语言一样,都有条件判断和循环语句。代码清单 2-7 是一个典型的 if... else if... else 语句。 ◆ 第 2 章 Android 自动化测试基础36 代码清单 2-7 Java 条件判断语句示例 1. if (1 == 1) 2. { 3. System.out.println("1 和 1 是相等的 !"); 4. } 5. else if ( 1 > 1 ) 6. { 7. System.out.println("1 大于 1!"); 8. } 9. else 10. { 11. System.out.println("1 小于 1!"); 12. } Java 语言中也有经典的 for 循环语句,代码清单 2-8 中第 2 行的第一个分号的子句定义 循环控制变量 i 及其初始值;第二个分号的子句指明循环终止条件,即 i 的值小于 names 数 组里的元素个数时执行循环,否则退出循环;第三个分号的子句指明每次循环对 i 值的处理 方式。 代码清单 2-8 Java 循环语句示例 1. String[] names = { "Android", "Test", "Debug" }; 2. for (int i = 0; i “ Java Project”项,如 图 2-2 所示。 图 2-2 在 Eclipse 中创建新测试工程 4)在新弹出的“ New Java Project”对话框中,在“ Project name :”文本框中输入新 Java 工程名称“chapter1.java”,单击“Finish”按钮创建工程。 图 2-3 在创建工程向导中输入工程名称 5)在 Eclipse 中右键单击新创建的工程,依次选择“ New”、“ Class”菜单项,如图 2-4 所示。 6)在随后弹出的“ New Java Class”对话框中,在“ Name:”文本框中输入“ Sample1”, 然后单击“Finish”按钮创建一个 Java 类,如图 2-5 所示。 7)在上一步新建的 Java 文件中输入如代码清单 2-9 所示的代码。 ◆ 第 2 章 Android 自动化测试基础38 图 2-4 在 Eclipse 中添加一个新 Java 类型 图 2-5 在创建向导中输入 Java 类型名称 代码清单 2-9 待测的 add 程序 public class Sample1 { public int add(int left, int right) { return left + right; } } 8)代码清单 2-9 中,待测程序是一个非常简单的 Java 函数,它实现了一个加法操作, 接受两个参数 left 和 right,并将两个参数的和返回给调用者。现在对这个函数编写一些 2.2 JUnit 简介 ◆ 39 JUnit 测试用例,在 Eclipse 中用右键单击刚创建的工程“ chapter1.java”,依次选择菜单项 里的“New”、“JUnit Test Case”,如图 2-6 所示。 图 2-6 在 Eclipse 中新建单元测试用例 9)在新弹出的“ New JUnit Test Case”对话框中选择“ New JUnit 3 test”单选框,并 在“ Name:”文本框中输入新 JUnit 测试用例名称“ Sample1Test”,然后单击“ Finish”按 钮创建测试用例,如图 2-7 所示。 图 2-7 在新建测试用例向导中输入用例名称 ◆ 第 2 章 Android 自动化测试基础40 10)这时 Eclipse 会弹出一个对话框,询问是否要将 JUnit 3 类库添加到编译路径中,单 击“OK”按钮选择默认设置,如图 2-8 所示。 图 2-8 处理添加 JUnit 引用对话框 11)在文件“Sample1Test.java”中输入代码清单 2-10 的代码。 代码清单 2-10 add 程序的普通单元测试用例 import junit.framework.TestCase; public class Sample1Test extends TestCase { public void testAdd() { assertEquals(3, new Sample1().add(1, 2)); } } 12)在 Eclipse 中单击工程,并依次选择菜单项“Run As”、“JUnit Test”运行创建的单 元测试用例,如图 2-9 所示。 图 2-9 执行 JUnit 单元测试用例 13)如果机器上已经安装了 Android 开发环境,Eclipse 可能会弹出对话框询问我们 是使用显示的 JUnit 执行环境还是使用 Eclipse 自带的 JUnit 执行测试用例,这里我们勾选 “ Use confi guration specifi c settings”复选框,并在“ Launchers”列表中选择“ Eclipse JUnit Launcher”,如图 2-10 所示。 14)执行完毕后就可以看到测试用例的执行结果,如图 2-11 所示。 在代码清单 2-10 中演示了 JUnit 测试用例的两个要求: 2.2 JUnit 简介 ◆ 41 图 2-10 选择测试用例执行程序 测试用例所在的类必须从 TestCase 类型中继承,这是因为在一个 JUnit 测试工程中, ❏ 并不是所有的 Java 类型都包含测试用例,有些 Java 类型可能是包含一些封装好的 测试辅助函数 Java 类型(是为测试用例服务的)。所以通过从 TestCase 类型继承一 个新类的方式告知 JUnit Launcher 类型是一个测试用例类。 每个测试函数的名字都以“ test”为前缀,没有参数,返回值必须是“ void”型, ❏ 并且声明为公开,这样 JUnit Launcher 才能将真正的测试用例函数和辅助测试的 函数区分开来。 图 2-11 运行 JUnit 测试结果 2.2.1 添加测试异常情况的测试用例 在代码清单 2-10 中,只是测试了加法运算的最正常的情形。但我们知道,在编程语言 ◆ 第 2 章 Android 自动化测试基础42 中,整型是有范围限制的,一般在进行单元测试时,需要考虑正常值、最大值、最小值、最 大值加一、最小值加一、最大值减一和最小值减一这七种情形。例如针对代码清单 2-9 中的 函数,尝试下面这两个值就会导致测试失败。 int a = new Sample1().add(2147483640, 8); assertTrue(a > 0); 这是因为整型 int 的最大值是 2147483647,2147483640 + 8 的结果 2147483648 已经超 出了整型的范围,从而导致溢出错误,而这个错误必须要报告给上层调用函数,以免上层函 数使用错误的值进行计算而得出错误的结果,从而导致难以追溯问题根源。在 Java 语言中, 一般通过异常来报告错误,这里我们修改代码清单 2-9 中的代码如代码清单 2-11 所示。 代码清单 2-11 导致整数溢出的加法运算 1. public class Sample2 2. { 3. public int add(int left, int right) 4. { 5. if (Integer.MAX_VALUE - left < right || 6. Integer.MAX_VALUE - right < left) 7. throw new ArithmeticException(String.format( 8. "%d 和 %d 相加会导致整数溢出 !", left, right)); 9. 10. return left + right; 11. } 12. } 并且在 Sample1Test.java 文件中添加一个新的测试用例,如代码清单 2-12 所示。 代码清单 2-12 加法整数溢出的测试用例 1. public void test 相加后整数溢出 () { 2. try { 3. new Sample2().add(2147483640, 8); 4. fail("2147483640 和 8 相加后,会导致整数溢出," + 5. " 函数应该检测到这个问题并抛出异常通知 !"); 6. } catch (ArithmeticException e) { 7. } 8. } 在上面的代码中,由于 JUnit 3 中没有类似 JUnit 4 的 ExpectedException 机制,所以在 测试用例中显式地将测试代码用 try... catch... 块包括起来了。在第 4、5 行中,我们用一个 显式的 fail 函数来测试异常没有抛出的情况,这是因为,如果在第 3 行 add 函数没有抛出异 常,那么程序就会执行第 4 行。 如果将代码清单 2-11 的第 6 行到第 8 行的参数检查代码删掉,再次运行代码清单 2-12 中的测试用例,可以敏锐地捕捉到这个错误,如图 2-12 所示。 2.2 JUnit 简介 ◆ 43 图 2-12 捕捉到错误的效果 在代码清单 2-12 中,笔者还用了一个小技巧,即将测试用例的函数名用中文命名,在 Java 语言中,是允许使用中文给函数和变量命名的。用中文为测试用例命名的好处就是直 观,而且还省掉了一些说明测试用例目的的注释。 2.2.2 测试集合 通常一个大的项目中会有很多测试用例。有时在对工程的某个模块做一些小改动时,由 于时间的关系,不能等到所有的测试用例都执行一遍,而是希望执行一些优先级高的测试用 例。除了将测试用例归到不同的工程这种分类手段以外,JUnit 还提供了一个测试集合(Test Suite)的概念来帮助分类测试用例。 接下来创建一个测试集合。 1)在 Eclipse 中新建一个 Java 类,并输入如代码清单 2-13 所示的代码。 代码清单 2-13 在 JUnit 中建立测试集合 1. import junit.framework.Test; 2. import junit.framework.TestSuite; 3. 4. public class SampleSuite extends TestSuite { 5. public static Test suite() { 6. TestSuite suite = new TestSuite("chapter1.java 工程里的所有测试 "); 7. suite.addTestSuite(Sample1Test.class); 8. suite.addTest(TestSuite.createTest(Sample2Test.class, 9. "test 相加后整数溢出 ")); 10. return suite; 11. } 12. } 2)需要在 Eclipse 中修改 JUnit 的执行方式显式指定要运行的测试集合,用右键单击工 程并依次选择“Run As”、“Run Confi guraitons...”。 图 2-13 修改 JUnit 的执行方式 3)在“ Run Confi gurations”对话框中,选择“ Test”页签,选中“ Run a single test” ◆ 第 2 章 Android 自动化测试基础44 单选框,并单击“ Search...”按钮找到在代码清单 2-13 中创建的测试集合“ SampleSuite”, 如图 2-14 所示。 图 2-14 指定要执行的 JUnit 的测试集合 4)最后单击“ Run ”按钮就会只执行“ SampleSuite ”中的测试用例了,如图 2-15 所示。 在代码清单 2-13 中,注意第 4 行,“ SampleSuite”类是从“ TestSuite”中继承下来的, 而不是之前的“TestCase”类;第 5 行定义了一个名为“suite” 的静态函数,这是 JUnit 要求的做法,JUnit 通过这种方式才 能发现测试集合的实际定义;第 6 行定义了这个测试集合的名 称,这个名称在 JUnit 的测试结果中会显示,见图 2-15 ;第 7 行通过调用 addTestSuite 函数添加测试集合中的测试用例,这 里 addTestSuite 的参数是测试类型,也就是说这个测试类型中 的所有测试用例都会被添加进测试集合中,如果要添加的测试 类型中有些测试用例不想被加进测试集合中,则需要把这些测 试用例放在另外的测试类型中,或者就像第 8 行一样,单独将 测试用例一个个添加进测试集合。 图 2-15 测试集合的运行结果 2.2 JUnit 简介 ◆ 45 2.2.3 测试准备与扫尾函数 在实际测试中,经常有这样的情况:在测试之前需要做一些准备工作,在测试之后又 要执行一些扫尾的操作。比如,为博客网站上的用于增、删、改博客的 API 编写测试用例, 都需要在调用 API 之前使用某个测试用户身份登录网站,在执行完测试之后,又需要注销 测试用户,以免影响后面的测试用例。为了满足这样的测试需求,JUnit 提供了测试用例 级别的准备与扫尾函数,也提供了测试集合级别的准备与扫尾函数,它们的名称都分别是 setUp 和 tearDown。 因为每个测试用例都需要登录和注销操作,所以可以在 setUp 函数中执行登录操作,并 在 tearDown 中执行注销操作。在执行测试用例时,JUnit 会保证在每个测试用例函数执行之 前,setUp 都会被调用;而无论测试用例是否是正常退出,tearDown 也都会被调用。代码清 单 2-14 演示的博客网站的增删改等测试用例的框架体现了这个方法: 代码清单 2-14 测试用例的准备和扫尾函数 1. import junit.framework.TestCase; 2. 3. public class 博客测试 extends TestCase { 4. public void setUp() { 5. System.out.println(" 用户登录 "); 6. } 7. 8. public void tearDown() { 9. System.out.println(" 用户注销 "); 10. } 11. 12. public void test 新增博客文章 () { 13. System.out.println(" 博客文章已添加 !"); 14. } 15. 16. public void test 修改博客 () { 17. System.out.println(" 博客文章已被修改 !"); 18. } 19. 20. public void test 删除博客 () { 21. System.out.println(" 查询要删除的博客文章 !"); 22. fail(" 找不到博客文章 !"); 23. System.out.println(" 博客文章已删除 !"); 24. } 25. } 运行结果如图 2-16 所示。 可以看到,虽然在代码清单 2-14 中,测试用例“ test 删除博客”在运行过程中会失 败(第 22 行),导致后面第 23 行的代码没有被执行,但是用户注销这一步还是被稳定地 执行了。 ◆ 第 2 章 Android 自动化测试基础46 在代码清单 2-14 中,每个测试用例运行时都要执行登录和注销操作,因此还有优化 的空间。我们可以只登录注销一次,批量测试博客的增、删、改工作,这就需要用到测试 集合,并且在测试集合运行前登录网站,测试集合运行完毕后注销用户,下面的代码清单 2-15 就演示了这个方法。 图 2-16 setUp 和 tearDown 函数的效果 代码清单 2-15 测试集合的准备和扫尾函数的使用示例 import junit.extensions.TestSetup; import junit.framework.Test; import junit.framework.TestSuite; public class 测试博客增删改用例集合 extends TestSuite { public static Test suite() { return new TestSetup(new TestSuite( 博客测试 2.class)) { protected void setUp() { System.out.println(" 用户登录 "); } protected void tearDown() { System.out.println(" 用户注销 "); } }; } } 2.3 Android 应用程序基础 ◆ 47 2.2.4 自动化测试用例编写注意事项 测试代码的稳定性高于一切。测试代码的编程与产品代码的编程有很大的不同,测试代 码的目的是测试代测产品是否实现了指定的功能需求。测试代码也有可能存在编程错误,因 此我们应该尽量避免测试用例本身错误导致的测试失败,否则就需要花较多时间去判断失败 是由用例引起的,还是由产品缺陷引起的。一般来说,自动化测试都是在晚上员工下班后执 行的,而且当前也有很多工具支持在多台机器上执行测试,因此测试代码的运行速度慢一点 关系不大,重要的是要保证稳定。 测试用例之间不能相互依赖。一般来说,JUnit 会按照测试用例函数在测试类型中的顺 序依次执行,代码清单 2-15 中用例的执行顺序是“ test 新增博客文章”、“ test 修改博客”、 “ test 删除博客”。但不能为了偷懒就在用例“ test 修改博客”中修改由用例“ test 新增博客 文章”创建的博客,并在用例“ test 删除博客”中删除它。这种做法的问题在于,如果新建 了一个测试集合,只添加了用例“ test 修改博客”,那么这个测试集合在执行的时候就会失 败,因为没有可供用例“test 修改博客”修改的博客。 测试用例之间不能相互影响。前面讲到的 JUnit 遵循测试四步法则的意义就在于此,由 于用例可以加入多个测试集合,在每个集合中用例放置的顺序是随机,对于测试用例来说, 并不知道在其之前和之后将运行哪个用例,所以一个好的测试用例应该尽量消除和恢复测试 过程中对测试环境的修改。比如代码清单 2-15 中的“ test 删除博客”这个用例,应该在执 行用例之前事先准备一个新的测试用博客,而不能删除一个固定的测试用博客,因为可能有 很多有关浏览博客的用例都依赖于它。 2.3 Android 应用程序基础 Android 应用大部分是使用 Java 语言编写的,Android SDK 工具包将源代码以及相关的 数据和资源文件全部打包到 Android 包中,这是一个后缀名为 .apk 的压缩文件,可以用解 压缩工具(例如 7-zip 等)解压 .apk 并查看其中的内容。一个 .apk 文件可以看成一个应用并 可以安装在 Android 设备上。 2.3.1 Android 权限系统 所有 Android 应用都运行在自己的安全沙盒里。 Android 操作系统是一个多用户的 Linux 操作系统,每个应用都是不同的用户。 ❏ 在默认情况下,系统为每个应用分配一个用户——这个用户只被系统使用,对应用是 ❏ 透明的。系统为应用的所有文件设置权限,这样一来只有同一个用户的应用可以访 问它们。 每个应用都有自己单独的虚拟机,这样应用的代码在运行时是隔离的,即一个应用 ❏ 的代码不能访问或意外修改其他应用的内部数据。 在默认情况下,每个应用都运行在单独的 Linux 进程中,当应用的任意一部分要被执 ❏ ◆ 第 2 章 Android 自动化测试基础48 行时(由用户显式启动或由其他应用发送的 Intent 启动),Android 都会为其启动一个 Java 虚拟机,即一个新的进程,因此不同的应用运行在相互隔离的环境中。当应用 退出或系统在内存不足要回收内存时,才会将这个进程关闭。 通过这种方式,Android 系统采用最小权限原则确保系统的安全性。也就是说,每个应 用默认只能访问满足其工作所需的功能,这样就创建了一个非常安全的运行环境,因为应用 不能访问其无权使用的功能,如图 2-17 所示。 Android 应用 / 进程空间 应用沙盒 -Linux 用户 ID:12345 应用沙盒 -Linux 用户 ID:54321 应用程序 Linux 用户 ID: 12345 应用程序 Linux 用户 ID: 54321资源: Linux 用户 ID: 12345 资源: Linux 用户 ID: 54321 文件 文件 网络 网络 传感器 传感器 两个应用运行在不同的进程里 (运行在不同的用户身份下) 数据库 数据库 日志 日志 ... ... SMS SMS 图 2-17 不同签名的 Android 应用运行在不同进程中 在 Android 系统中,可以用 ps 命令来查看系统为每个应用分配的用户 ID,如代码清单 2-16 所示。 代码清单 2-16 查看 Android 系统应用的用户 ID 的命令 student@student:~$ adb shell ps USER PID PPID VSIZE RSS WCHAN PC NAME root 1 0 296 204 c009b74c 0000ca4c S /init ...... radio 31 1 5392 704 ffffffff afd0e1bc S /system/bin/rild root 32 1 102056 25864 c009b74c afd0dc74 S zygote root 36 1 740 328 c003da38 afd0e7bc S /system/bin/sh ...... root 39 1 3400 192 ffffffff 0000ecc4 S /sbin/adbd app_3 118 32 137656 20824 ffffffff afd0eb08 S com.android.inputmethod.latin 2.3 Android 应用程序基础 ◆ 49 radio 122 32 146648 22864 ffffffff afd0eb08 S com.android.phone app_25 123 32 146184 24216 ffffffff afd0eb08 S com.android.launcher system 129 32 137096 19312 ffffffff afd0eb08 S com.android.settings ...... app_22 185 32 132052 18472 ffffffff afd0eb08 S com.android.music ...... app_15 231 32 144660 19812 ffffffff afd0eb08 S com.android.mms app_30 248 32 135192 20256 ffffffff afd0eb08 S com.android.email app_28 262 32 130696 17516 ffffffff afd0eb08 S com.svox.pico app_36 279 32 135252 20176 ffffffff afd0eb08 S cn.hzbook.android.test.chapter1 ...... 从代码清单 2-16 中可以看到,最左边的一列是应用的用户 ID,第二列是应用的进程 ID,可以看到前 39 个进程大部分都运行在 root 权限下,因此这些进程对整个系统拥有绝 对的访问权;而从进程 ID 为 118 的应用开始,基本上每个应用都分配了一个独立的用户名 (用户名以 app 开头)。而在 Android 系统中,/data/data 文件夹用于存放所有应用(包括在 / system/app、/data/app 和 /mnt/asec 等文件夹中安装的软件)的数据信息。在 /data/data 文件 夹中,每个应用都有自己的文件夹存取数据,文件夹默认以应用的包名命名,而文件夹的所 有者就是系统分配给应用的用户,可以用“ ls-l”命令列出这些文件夹的详细信息,如代码 清单 2-17 所示。 代码清单 2-17 查看 Android 系统应用数据的所有者用户 student@student:~$ adb shell ls-l /data/data drwxr-x--x app_36 app_36 2012-11-09 11:22 cn.hzbook.android.test. chapter1 drwxr-x--x app_5 app_5 2012-11-03 18:04 com.android.cardock drwxr-x--x system system 2012-11-03 18:04 com.android.server.vpn ...... drwxr-x--x app_16 app_16 2012-11-03 18:05 com.android.fallback drwxr-x--x app_2 app_2 2012-11-03 18:05 com.android.gallery drwxr-x--x app_17 app_17 2012-11-03 18:05 com.android.carhome drwxr-x--x app_0 app_0 2012-11-09 11:25 com.android.contacts drwxr-x--x app_18 app_18 2012-11-03 18:05 com.android.htmlviewer ...... drwxr-x--x system system 2012-11-03 18:10 com.android.settings ...... 在代码清单 2-17 的输出中,第一列是用户权限设置,第二列是文件夹的所有者,第三 列是文件夹所有者所在的用户组,输出的第一行表示应用“ cn.hzbook.android.test.chapter1” 的数据只有用户“ app_36”才能访问,而“ app_36”恰好是在代码清单 2-16 查看 Android 系统应用的用户 ID 的命令的输出中系统分配给应用的用户名。通过限定应用数据的所有 者是分配给应用的用户的方式,Android 系统有效地防范了其他应用非法读写应用私有的 数据。 ◆ 第 2 章 Android 自动化测试基础50 但是不同的应用程序也可以运行在相同的进程中,要实现这个功能,首先必须使 用相同的密钥签名这些应用程序,然后必须在 AndroidManifest.xml 文件中为这些应用 分 配 相 同 的 Linux 用 户 ID, 这 要 通 过 用 相 同 的 值 / 名 定 义 AndroidManifest.xml 属 性 android:sharedUserId 才能做到。 Android 应用 / 进程空间 应用沙盒 -Linux 用户 ID:12345 应用程序 Linux 用户 ID: 12345 应用程序 Linux 用户 ID: 54321 资源: Linux 用户 ID: 12345 文件 网络 传感器 拥有同样签名的两个应用运行在相同的进程中 (相同的用户身份下) 数据库 日志 ... SMS 图 2-18 相同签名的应用可以运行在同一个进程中 由于 Android 系统提供多种多样的 API 来允许应用访问设备硬件(例如拍照应用)、 WIFI、用户数据和设备设置等,有些 API 需要进行特别处理才能防止应用被滥用,例如, 没人希望一个应用在后台运行时仍然通过网络 API 访问 3G 网络浪费流量。Android 采用在 应用安装时执行权限检查的策略来控制应用访问与用户隐私相关的数据和执行不安全的操 作。每个应用都必须显式声明所需要的权限,在安装应用时 Android 系统会提示用户每个应 用需要哪些权限并要求用户同意才能继续安装。这种在安装时进行权限控制策略有效地帮助 最终用户保护自己的隐私并避免受到恶意攻击。因为 Android 用户通过多种 Android 应用市 场来安装应用,这些应用的质量和可信任度的差别非常大,所以 Android 系统默认将所有应 用都当作不稳定和邪恶的。Android 2.2 定义 134 种权限,分成三类。 用以控制调用一些无害但是会让人烦躁的 API 的普通权限。比如权限 SET_WALLPAPER ❏ 用来控制修改用户背景图片的能力。 用以控制支付和收集隐私等危险 API 调用的高危权限。比如发短信和读取联系人列 ❏ 表这些功能就要求高危权限。 用以控制运行后台程序或删除应用等危险操作的系统权限。获取这些权限非常难:只 ❏ 有使用设备生产商密钥签名的程序才有 Signature 权限,而只有安装在特定的系统文 件夹的应用才有 SignatureOrSystem 权限。这些限制确保了只有设备厂商预装的应用 2.3 Android 应用程序基础 ◆ 51 才有能力获取这些权限,而所有其他应用试图获取这些权限的要求都会被系统忽略。 当应用需要与其他应用共享数据,或者应用需要访问系统服务时: 可以为两个应用分配相同的 Linux 用户 ID,这样一来两个应用可以互相访问对方的 ❏ 文件。为了节省系统资源,同一个用户 ID 的应用也可能会运行在同一个 Linux 进程 中并共享同一个虚拟机。为了让多个应用分配有相同的用户 ID,这些应用必须使用 同一个数据签名。 一个应用可以申请访问敏感数据的权限,但是必须在安装应用的时候由用户显式同 ❏ 意才会被授权。这些敏感数据包括用户的联系人信息、短消息、SD 卡存储、相机、 蓝牙服务等。 2.3.2 应用的组成与激活 1.应用组件 在 Android 系统中,一共有四种应用组件,每个组件都有不同的目的和生命周期。 (1)活动(Activity) 用户界面上每个屏幕就是一个活动,例如,一个邮件应用会有一个活动用于展示邮件列 表界面,一个活动用于写邮件,还有一个活动用于读邮件。虽然这些活动组成了一个完整的 邮件应用,但是它们相互是独立的。也就是说,另一个应用可以随时启动它们中的任意一 个。比如为了发送用户的照片,一个照片应用就可以直接启动邮件应用的写邮件的活动。 (2)后台服务(Service) 后台服务是在后台运行,用于处理长时间任务而不影响前台用户体验的组件。后台服务 没有用户界面。比如,用户正在前台使用一个应用时,一个后台服务同时在后台播放音乐。 再比如,当应用在通过网络下载大量数据时,为了避免影响前台与用户的交互,也会通过后 台服务下载数据。Android 的其他组件,例如一个活动,可以启动后台服务,也可以绑定到 一个后台服务上与它交互。 (3)内容供应组件(Content Provider) 内容供应组件用来管理应用的可共享部分的数据。例如应用可以将数据存储在文件系 统、SQLite 数据库、网络或任何一个应用可以访问的永久存储设备。通过内容供应组件, 其他应用可以查询、设置和修改应用的数据。例如 Android 系统提供了一个内容供应组件 来管理用户的联系人信息,其他应用只要有相应的权限都可以通过查询内容供应组件(例如 ContactsContract.Data)来读取和修改某个联系人的信息。 即使应用不需要共享数据,也可以通过内容供应组件来读取和修改应用的私有数据,比 如 Android 示例程序 NotePad 就使用内容供应组件来保存笔记。 (4)广播接收组件(Broadcast Receivers) 广播接收组件是用来响应系统层面的广播通知的组件。系统会产生很多广播,比如通知 关闭屏幕的广播和电池电量低的广播。应用本身也可以广播通知,例如告诉其他应用有些数 ◆ 第 2 章 Android 自动化测试基础52 据已经下载完毕需进行处理的广播。虽然接收广播并不要求有用户界面,但是一般会显示状 态栏消息通知用户有个广播事件发生了。 Android 系统的特别之处是它将任意一个应用可以复用其他应用的部分组件作为设计时 的关键考量。比如,我们开发的应用 A 允许用户通过相机设备抓取一张照片,而用户的手 机上可能已经安装有拍照应用 B 了,与其在应用 A 中重复编写代码从相机上拍取一张照片, 不如通过复用现有拍照应用 B 的组件得到照片。在 Android 系统中,应用 A 甚至不需要链 接应用 B 的任何代码,只需要简简单单地在应用 A 中启动应用 B 拍摄一张照片,在完成拍 照后,应用 B 会将拍摄的照片返回给应用 A,由应用 A 使用。对最终用户来说,看起来拍 照应用 B 好像就是应用 A 的一部分。 系统在启动一个组件时实际上会为组件所属的应用启动一个进程,并且初始化组件用到 的所有类型。例如,当应用启动了照相应用并用于拍照活动时,活动实际上是运行在照相应 用自己的进程中的。因此,与其他大部分操作系统不同的是,Android 应用是没有单一的入 口点(类似其他操作系统中程序的 main 函数)的。 因为系统将不同应用运行在自己单独的进程中,而这个进程又拥有自己的文件系统访问 权限以限制其他进程的访问,所以一个应用是无法直接激活其他应用的某个组件的,必须通 过 Android 系统本身来激活。为了激活其他应用的组件,必须通过向系统传递消息指明要激 活的组件,由系统来激活它。 2.组件的激活 前面说 4 个组件,其中活动、后台服务和广播接收这 3 种组件都可以通过一个叫做意 图(Intent)的异步消息激活。可以将意图想象成一个要求其他组件执行某个操作的信使, 它附带有协同两个组件工作方式的消息,而不管这两个组件是包含在同一个应用还是两个应 用中。 在图 2-19 中,用户首先打开 Gmail 应用查看邮件列表。在用户从列表中选择一封邮件 后,邮件列表活动会发送一个“查看消息”的意图,这个意图由 Android 系统(而不是由应 用本身)处理。Android 系统发现 Gmail 本身的“查看邮件具体信息”活动可以满足这个意 图,因此在 Gmail 进程内启动它。要阅读的邮件中有一个外部网页链接,当用户单击这个链 接时,由于 Gmail 本身并没有查看处理网页浏览的能力,因此 Android 系统启动浏览器,另 外一个进程打开网页。而当浏览的网页要打开一个视频时,浏览器又通过发送一个“观看视 频”的意图向 Android 系统请求外部进程打开视频。虽然在这个过程中实际上启动了 3 个进 程,但是用户感觉浏览器和用于观看视频的 YouTube 程序都是 Gmail 应用的一部分。由于 匹配意图和活动的过程是 Android 系统的工作,Gmail 应用在向系统发出“打开网站”意图 的时候,甚至都不需要去了解哪个应用会去处理这个意图,Android 系统就巧妙地解决了应 用层面上的代码复用问题。 对于活动和后台服务,一个意图对象定义了要执行的操作(例如“查看”或“发送”什 么东西),或许还会指定要处理的数据的 URI 地址及启动组件所需要的其他数据。比如,一 个意图在请求活动显示图片或打开网页时,会包含要显示的图片地址或网页地址,有的时 2.3 Android 应用程序基础 ◆ 53 候,被请求的活动在处理完请求后,会将结果放到意图中返回给发出请求的组件,例如,发 送邮件的组件 A 发出一个意图请求组件 B 显示联系人列表供用户选择,用户在组件 B 上选 择了邮件收件人后,可以将所选的联系人信息放回意图中返回给组件 A。 查看邮件具体信息 活动(Activity) 查看邮件列表 活动(Activity) WebView 活动(Activity) 浏览器 观看视频 活动(Activity) 观看视频 意图(Intent) 打开网站 意图(Intent) 查看消息 意图 (Intent) Dalvik VM Dalvik VM Dalvik VM 图 2-19 使用意图(Intent)打开多个活动(Activity) 而对于广播组件,意图中可能就只定义需要广播的数据,例如在系统电量很低的时候, 广播的意图就只包含了一个已知的动作用于指明“电池没电了”,由系统将其广播到正在运 行的各个活动上,每个活动根据需要决定是否处理意图。如图 2-20 所示。 Intent Intent Intent Intent XXXXX A XXXXX XXX XXXXXX XX XXX XXXXX XXX XXXXXX XX XXXXX XXX XXXXXX XX XXX 图 2-20 使用意图广播隐式激活应用组件 ◆ 第 2 章 Android 自动化测试基础54 但内容供应组件不是通过意图来激活的,而是通过响应一个内容解析(ContentResolver) 请求被激活的,详细方式不属于本书要讲解的内容,不再多述。 2.3.3 清单文件 在 Android 系统启动应用的一个组件之前,需要读取应用的 AndroidManifest.xml 文件 也叫清单文件来获知应用中是否包含该组件。AndroidManifest.xml 文件在应用工程的根目 录下,必须将应用中的所有组件在该清单文件里声明。 除了列出应用中包含的所有组件列表,清单文件中还包含下面这些信息: 应用需要申请的权限,比如访问网络或读取用户的联系人列表。 ❏ 应用要求的最低的 Android 系统版本。 ❏ 应用将会用到的硬件或软件功能,比如是不是要用到相机、蓝牙设备和触摸屏等。 ❏ 应用要用到的非 Android 标准开发库,例如 Google 地图 API。 ❏ 1.声明组件列表 清单文件的主要工作是列明应用所包含的组件列表,例如代码清单 2-18 的清单文件表 明应用包含一个 Activity。 代码清单 2-18 在清单文件中指明应用包含的活动 1. 2. 3. 4. 6. 7. ... 8. 9. 节 点,android:icon 属 性 指 向 应 用 图 标 在 资 源 文 件 中 的 位 置。 在 节点,android:name 属性说明了应用所包含的 Activity 的全名,而 android:label 属性则声明了在 Activity 运行时 Android 系统显示该 Activity 的标题。 应用中所有组件应该以下面的方式声明: 节点声明了应用中的活动。 ❏ 节点声明了后台服务。 ❏ 节点声明了接收广播的组件。 ❏ 节点声明了内容供应组件。 ❏ 如果活动、后台服务、内容供应这些组件没有在清单中声明,即使它们在源代码中已经 实现了,对于系统来说也是不可见的,因此也没有办法运行它们。但是一个广播的接收器 除了可以在清单文件中声明以外,还可以在应用运行时动态创建并通过调用 registerReceiver 2.3 Android 应用程序基础 ◆ 55 系统 API 来注册。 2.声明组件的能力 在前面说过,可以显式在意图中指明目标组件的名称来启动活动、后台服务和广播接收 等组件。但是,意图真正强大的地方是意图动作(Intent Action)。通过意图动作,只需要在 意图中指明动作的类型和执行动作所要求的数据,系统自己会找到可以执行该动作的组件并 启动它。如果有系统中有多个组件可以执行该动作,那么系统会让用户选择一个组件。 Android 系统是通过对比组件清单文件里的意图漏斗(Intent Filter)和请求的意图动作 来找到可以满足要求的组件。可以在清单文件中为组件元素添加一个 节点来 声明一个意图漏斗。例如,一个邮件程序中包含了写邮件的活动,可以通过在清单文件中加 上其可响应“send”的意图漏斗,另外一个应用可以创建一个意图对象,并且标注意图动作 是“ send”,在将意图对象传递给 startActivity 函数时,系统看到邮件程序的写邮件的活动 的意图漏斗并满足该意图,然后启动它。 3.声明应用的必备条件 Android 系统可支持多种设备。不同的设备所具备的功能是不一致的。为了避免用户将 应用安装在其不支持的设备上,比如缺少应用所必需的功能,需要在清单文件中详细列出应 用所要求的硬件和软件功能。一般来说这些都是资讯类的信息,Android 系统不会去读取它。 而 Android 应用商店(例如 Google Play)会根据清单文件列明的硬件要求,在用户搜索应用 时过滤应用,或者在用户尝试安装应用时提示用户。 如果应用需要使用相机,那么必须在清单文件中声明了这项要求,这样一来,在 Google Play 等应用商店中,针对没有相机的设备,就不会显示这个应用。当然了,即使应 用要求使用相机,也可以不在清单文件中列明该项要求,因此应用商店不会限制用户在没有 相机的设备中安装应用,但应用应该在运行时动态检测设备是否配备相机,没有则禁用掉相 应的功能。 在开发 Android 应用时需要考虑的以下重要的设备特性。 屏幕大小和像素密度 ❏ 。为了根据设备使用的屏幕对其归类,Android 系统采用两个归 类维度:屏幕的物理尺寸和像素密度,即一英寸屏幕可以显示多少个像素。为简单起 见,Android 系统对屏幕的分类方法是: 按屏幕尺寸分为小屏、中屏、大屏和超大屏。  按像素密度分为低、中、高和极高密度。  Android 系统会根据设备屏幕调整应用的界面布局和图片,因此在默认情况下应用应该 兼容所有的屏幕尺寸和密度。但是为了提供更良好的用户体验,应该为不同的屏幕尺寸制定 特定的界面布局,针对不同的屏幕像素密度显示特定的图像。 设备特性 ❏ 。不同的设备会采用不同的硬件配置,比如相机、蓝牙设备、特定版本的 OpenGL 和光敏感元件并不是在每台设备上都有的。因此在应用的清单文件中必须使 用 节点指明应用需要用到的设备特性。 ◆ 第 2 章 Android 自动化测试基础56 平台版本 ❏ 。每个 Android 版本都会增添一些新的功能,如果应用使用的功能是新版本 Android 系统才提供的,那么在应用的清单文件中,也应该用 节点指明应 用要求 Android 系统的最低版本。 2.3.4 Android 应用程序的单 UI 线程模型 虽然 Android 支持多线程编程,但只有 UI 线程也就是主线程才可以操作控件,如果在 非 UI 线程中直接操作 UI 控件,会抛出 andorid.view.ViewRoot$CalledFromWrongThreadExc- eption: Only the original thread that created a view hierarchy can touch its views。这是因为 UI 操作不是线程安全的,如果允许多线程同时操作 UI 控件,可能会发生灾难性结果。假设线 程 A 和 B 均可直接操作文本框 T,A 希望将 T 显示的文本更新为“ iphone5 很烂!”,B 则希 望将 T 的文本更新为“ android 很不错!”,当 A 将 T 的文本更新到“ iphone5 很烂”(感叹号 还没有绘制)时,线程调度程序将 A 暂停,转入执行线程 B 的代码,线程 B 则从头开始更 新 T 要显示的文本,比如更新到“ android”又被调度程序暂停,切换到 A,线程 A 恢复代 码执行后补全尚未绘制的感叹号,这样一来,T 的文本实际上就变成了“ android 很烂!”。 虽然可以通过线程同步的方式规定线程 A 和 B 操作控件 T 的顺序,但是由于这种编程方式 不容易掌握,因此 Android 系统索性限制只有 UI 线程才能操作控件。 但是有的时候后台线程需要更新控件显示信息,例如一个在后台下载图片的程序在完成 下载后,需要更新 UI 提醒用户。Android 采用消息队列的机制来满足这个要求,如图 2-21 所示。 后台线程 后台线程 硬件 UI 线程 (主线程) 循环处理 消息 UI 消息队列 Handler Handler UI 事件 UI 事件 UI 事件 UI 事件 UI 事件 UI 事件 图 2-21 Android 的单 UI 线程模型 在消息队列机制中,不管是硬件(如触摸屏)还是后台线程,都可以向消息队列中放入 UI 消息,UI 线程循环处理消息队列的消息,按消息的语义更新控件状态。 2.4 本章小结 ◆ 57 由于 UI 线程负责事件的监听和绘图,因此,必须保证 UI 线程能够随时响应用户的需 求,UI 线程中的操作应该向中断事件那样短小,费时的操作(如网络连接)需要另开线程, 否则,如果 UI 线程超过 5 秒没有响应用户请求,会弹出对话框提醒用户终止应用程序。 2.4 本章小结 本章摘要讲解了 Java 编程语言、JUnit 用法和 Android 系统实现方式等知识,这些知识 是编写 Android 自动化测试用例必须要了解的。建议编程初学者在编程过程中采用类似 2.1 节的代码封装方式,即自下而上的封装方式,不要在编写代码的初期去考虑今后代码是否会 复用,先将代码用最简单、稳定的方式掌握住,当发现代码会在其他地方重复两次以上时, 再将代码封装成一个函数,进而再封装成类,逐步提炼代码。如果一开始就考虑代码的复用 性,会导致函数过于复杂,这不光影响开发进度,还可能因为使用场景考虑得不全面,导致 封装的函数适用性不高。 第 3 章 Android 界面自动化白盒测试 本章讲解在对待测应用源码有一定了解的基础上进行 Android UI 自动化白盒测试的方法。 3.1 Instrumentation 测试框架 Android 系统的 Instrumentation 测试框架和工具允许我们在各种层面上测试应用的方方 面面。该测试框架有以下几个核心特点: 测试集合是基于 JUnit 的。既可以直接使用 JUnit ,不调用任何 Android API 来测试 ❏ 一个类型,也可以使用 Android JUnit 扩展来测试 Android 组件。 Android JUnit 扩展为应用的每种组件提供了针对性的测试基类。 ❏ Android 开发工具包(SDK)既通过 Eclipse 的 ADT 插件提供了图形化的工具来创建 ❏ 和执行测试用例,也提供了命令行的工具,以便与其他 IDEs 集成,这些命令行工具 甚至可以创建 ant 编译脚本。这些工具从待测应用的工程文件中读取信息,并根据这 些信息自动创建编译脚本、清单文件和源代码目录结构。 3.1.1 Android 仪表盘测试工程 与 Android 应用类似,Android 测试用例也是以工程的形式组织。一般推荐使用 Android 自带的工具来创建测试工程,这是因为: 自 动 为 测 试 包 设 置 使 用 InstrumentationTestRunner 作 为 测 试 用 例 执 行 工 具, 在 ❏ Android 中必须使用 InstrumentationTestRunner(或其子类)来执行 JUnit 测试用例。 为测试包创建一个合适的名称。如果待测应用的包名是 com.mydomain.myapp,那么 ❏ 工具会将测试用例的包名设为 com.mydomain.myapp.test。这样可以帮助我们识别用 例与待测应用之间的联系,并且规避类名冲突。 会创建好必要的源码目录结构、清单文件和编译脚本,帮助我们修改编译脚本和清 ❏ 单文件以建立测试用例与待测应用之间的联系。 3.1 Instrumentation 测试框架 ◆ 59 虽然可以将测试用例工程保存在文件系统的任意位置,但一般的做法是将测试用例工程 的根目录“ tests/”放在待测应用工程的根目录下,与其源文件目录“ src/”并列放置。比 如说,如果待测应用工程的根目录是“MyProject”,那么按照编程规范,应该采用下面的目 录结构: MyProject/ AndroidManifest.xml res/ ……(主应用中的资源文件) src/ ……(主应用的源代码) tests/ AndroidManifest.xml res/ ……(测试用例的资源文件) src/ ……(测试用例的源代码) 在 1.3 节已经讲过使用 Eclipse 图形化工具创建 Android 测试工程的方法,这里讲解使 用命令行工具创建测试工程的方法。在随书配套资源中附带的虚拟机中,home 目录下有个 名为“ practice”的文件夹,读者可以在其中尝试书中的例子,如果操作有误需要恢复原始 练习,可以从“practice-backup”中还原。 1)这里复用第 1 章演示的工程“ cn.hzbook.android.test.chapter1”,首先进入工程的主 目录: student@student:~$ cd practice/chapter3/cn.hzbook.android.test.chapter1/ 2)使用“android”命令创建测试工程: $ android create test-project -m ..-p tests “ android”命令是一个命令集合,其很多功能都是通过子命令,甚至是二级子命令完 成的。在上例中,“ create”就是一级子命令,而“ test-project”就是二级子命令。“ test- project”接受表 3-1 中的几个参数。 注意 在本书配套资源的虚拟机中,已经将 Android SDK 的目录添加进程序搜索路径环境变 量 PATH 中,如果读者没有使用虚拟机,请自行设置。 表 3-1 android create test-project 命令参数清单 参 数 名 说  明 -m 这是一个必填选项,待测应用工程的主目录,该路径是其相对于测试工程的相对路径。在上例 中,“ ..”表明待测应用工程的主目录。“ cn.hzbook.android.test.chapter1”是新的测试工程的上级目 录,也就是说测试工程放在“cn.hzbook.android.test.chapter1”目录中 ◆ 第 3 章 Android 界面自动化白盒测试60 参 数 名 说  明 -n 测试工程的名称,这是一个可选项 -p 必填项,测试工程的主目录名。上例中指定测试工程应该保存在文件夹“ cn.hzbook.android.test. chapter1”中一个叫“tests”的目录里。如果文件夹不存在,“android”命令会创建它 3)在正常情况下,前面的命令应该会显示如下输出,笔者采用类似 bash 注释的方式批 注说明其意义: # 首先 "android" 工具确认待测应用的包名,以便修改测试工程里的 AndroidManifest.xml # 文件的 "targetPackage" 属性 Found main project package: cn.hzbook.android.test.chapter1 Found main project activity: .MainActivity # 确认待测应用要求的最低 Android 版本,以便在执行测试用例时,知道启动哪个版本的模拟器 # 或者设备 Found main project target: Android 2.2 # 创建测试工程的主目录 Created project directory: tests # 创建测试工程的源码目录树结构 Created directory /home/student/practice/chapter3/cn.hzbook.android.test. chapter1/tests/src/cn/hzbook/android/test/chapter1 # 针对待测应用的每个活动,根据测试用例模板分别创建一个测试用例源文件 Added fi le tests/src/cn/hzbook/android/test/chapter1/MainActivityTest.java # 创建保存测试用例可能会用到的资源文件的目录 Created directory /home/student/practice/chapter3/cn.hzbook.android.test. chapter1/tests/res # 创建测试用例的编译输出文件夹 Created directory /home/student/practice/chapter3/cn.hzbook.android.test. chapter1/tests/bin # 创建保存测试用例可能会引用到的 jar 包的目录,在编译打包测试用例工程时,Android 系统 # 会自动将 libs 文件夹中的 jar 包打包到最终的测试用例应用中 Created directory /home/student/practice/chapter3/cn.hzbook.android.test. chapter1/tests/libs # 创建清单文件和编译脚本等文件 Added fi le tests/AndroidManifest.xml Added fi le tests/build.xml Added fi le tests/proguard-project.txt 4)测试工程创建好以后,就可以向其中添加测试代码了。 Android 仪表盘测试框架是属于白盒测试范畴,一般来说需要有待测应用的源代码级别 的知识才能开展测试,在后文我们也将看到如何在脱离源码的情况下编写仪表盘测试用例。 这种测试技术依赖 Android 系统的仪表盘技术,在编写测试代码之前,先来看看仪表盘技术。 3.1.2 仪表盘技术 在应用启动之前,系统会创建一个叫做仪表盘的对象,用来监视应用和 Android 系统之 (续) 3.1 Instrumentation 测试框架 ◆ 61 间的交互。仪表盘对象通过向应用动态插入跟踪代码、调试技术、性能计数器和事件日志的 方式,来操控应用。 Android 仪表盘对象是 Android 系统中的一些控制函数或者说是钩子(hook),这些钩 子独立控制 Android 组件的生命周期并控制 Android 加载应用的方法。通常一个 Android 组件的生命周期由系统决定。例如一个活动对象的生命周期始于响应意图而被激活,先是 调用活动的 onCreate() 函数,接着调用 onResume()。当用户启动其他应用时,onPause() 函数就会被调用。而如果活动中的代码调用了 fi nish() 函数,那么就会触发 onDestroy() 函 数。Android 系统并没有提供直接的 API 允许我们调用这些回调函数,但可以通过仪表盘对 象在测试代码中调用到它们,这样一来允许我们监视组件生命周期的各个阶段。代码清单 3-1 演示了测试活动保存和恢复状态的方法,其首先设置下拉框到一个指定的状态(分别是 “ TEST_STATE_DESTROY_POSITION ” 和“ TEST_STATE_DESTROY_SELECTION ”), 接着通过重启活动来验证活动是否正确保存和恢复重启前下拉框的状态。 代码清单 3-1 调用 Activity 类的 API 来测试活动回调函数 1. public void testStateDestroy() { 2. /* 3. * 指定活动里下拉框的值和位置,以便后续验证中使用。 4. * 测试执行的时候系统会将测试用例应用和待测应用放在同一个进程中 5. */ 6. mActivity.setSpinnerPosition(TEST_STATE_DESTROY_POSITION); 7. mActivity.setSpinnerSelection(TEST_STATE_DESTROY_SELECTION); 8. 9. // 通过调用 Activity.fi nish() 关闭活动 10. mActivity.fi nish(); 11. // 调用 ActivityInstrumentationTestCase2.getActivity() 来重启活动 12. mActivity = this.getActivity(); 13. 14. /* 15. * 再次获取活动中下拉框的值和位置 16. */ 17. int currentPosition = mActivity.getSpinnerPosition(); 18. String currentSelection = mActivity.getSpinnerSelection(); 19. // 测试重启前后的值是相同的 20. assertEquals(TEST_STATE_DESTROY_POSITION, currentPosition); 21. assertEquals(TEST_STATE_DESTROY_SELECTION, currentSelection); 22. } 注意 在随书配套资源的虚拟机上,创建 Android 示例代码的方式是在 Eclipse 的新建工程对 话框中选择“Android”、“Android Sample Project”项,并使用“Android 4.1”作为“Build Target”,最后选择里面的示例工程即可阅读和学习示例代码。 这里面的关键函数是仪表盘对象里的 API getActivity(),只有调用了这个函数,待测活 ◆ 第 3 章 Android 界面自动化白盒测试62 动才会启动。在测试用例里,可以在测试准备函数中做好初始化操作,然后再在用例中调用 它启动活动。 在前面的代码中,我们也看到仪表盘技术可以将测试用例程序和待测应用放在同一个进 程中,通过这种方式,测试用例可以随意调用组件的函数,查看和修改组件内部的数据。 与 Android 应用其他组件一样,也需要在 AndroidManifest.xml 文件中通过 标签声明仪表盘对象,例如代码清单 3-2 就是上例仪表盘的声明。 代码清单 3-2 仪表盘在清单文件里的声明 “ targetPackage”属性指明了要监视的应用,“ name”属性是执行测试用例的类名,而 “label”则是测试用例的显示名称。 代码清单 3-1 还是通过调用 Activity 类公开的函数控制活动(Activity)等 Android 应 用组件的生命周期,也可以用 Instrument 类型提供的辅助 API 调用到活动的 onPause 和 onResume 等回调函数,比如代码清单 3-3 同是“ SpinnerTest”中的示例代码,演示了调用 回调函数的操作方法: 代码清单 3-3 调用仪表盘 API 来测试活动回调函数 1. /* 2. * 验证待测活动在中断并恢复执行后依然能恢复下拉框的状态 3. * 4. * 首先调用活动的 onResume() 函数,接着通过改变活动的视图 5. * 来修改下拉框的状态。这种做法要求整个测试用例必须运行在 UI 6. * 线程中,因此与其在 runOnUiThread() 函数中执行测试代码, 7. * 本例直接在测试用例函数上加 @UiThreadTest 属性 8. */ 9. @UiThreadTest 10. public void testStatePause() { 11. // 获取进程中的仪表盘对象 12. Instrumentation instr = this.getInstrumentation(); 13. 14. // 设置活动中下拉框的位置和值 15. mActivity.setSpinnerPosition(TEST_STATE_PAUSE_POSITION); 16. mActivity.setSpinnerSelection(TEST_STATE_PAUSE_SELECTION); 17. 18. // 通过仪表盘对象调用正在运行的待测活动的 onPause() 函数。它的 19. // 作用跟 testStateDestroy() 里的 fi nish() 函数调用是完全一样的 20. instr.callActivityOnPause(mActivity); 21. 22. // 设置下拉框的状态 23. mActivity.setSpinnerPosition(0); 24. mActivity.setSpinnerSelection(""); 25. 3.1 Instrumentation 测试框架 ◆ 63 26. // 调用活动的 onResume 函数,这样强制活动恢复其前面的状态 27. instr.callActivityOnResume(mActivity); 28. 29. // 获取恢复的状态并执行验证 30. int currentPosition = mActivity.getSpinnerPosition(); 31. String currentSelection = mActivity.getSpinnerSelection(); 32. assertEquals(TEST_STATE_PAUSE_POSITION,currentPosition); 33. assertEquals(TEST_STATE_PAUSE_SELECTION,currentSelection); 34. } 在第 20 行,通过 Instrumentation.callActivityOnPause 函数调用了待测活动的 onPause() 函数,这是因为 Android 系统中的 Activity.onPause 函数是被保护的(protected),也就是说 除了 Activity 类自己和其子类的代码,其他代码都无法调用到这个函数。onPause() 这个回 调函数只有在系统将活动置于后台,并没有将其杀掉之前调用。比如当前有活动 A 运行在 系统中,当用户启动活动 B(其将运行在活动 A 之上),Android 会调用活动 A 的 onPause() 函数,而系统需要等到这个函数调用完毕之后才会创建活动 B,因此不能在这个函数里执行 一个长时间的操作。一般来说 onPause() 函数用来保存活动在编辑时的中间状态,以便在活 动置于后台时,万一系统资源不够时将活动杀掉,当用户再次重启活动时,不会丢失之前编 辑的数据。这个函数也经常会用来停止一些消耗资源的操作(例如动画),以及释放独占性 的资源(例如相机的访问)。可以看到这个函数和 Activity.onResume() 函数对用户体验来说 都是很关键的函数,而 Android 的 API 并没有提供一个直接的调用方式,因此只能通过仪表 盘 API 来触发并测试它们。 3.1.3 Instrumentation.ActivityMonitor 嵌套类 仪表盘对象是用来监视整个应用或者所有待测活动(Activities)与 Android 系统交互的 所有过程,而 ActivityMonitor 嵌套类则是用来监视应用中单个活动的,它可以用来监视一 些指定的意图。创建好 ActivityMonitor 的实例后,通过调用 Instrumentation.addMonitor 函 数来添加这个实例。当活动启动后,系统会匹配 Instrumentation 中的 ActivityMonitory 实例 列表,如果匹配,就会累加计数器。 本书的示例工程“ chapter3/cn.hzbook.android.test.chapter3.activitymonitor”和它的测试 工程“chapter3/cn.hzbook.android.test.chapter3.activitymonitory.test”就演示了 ActivityMonitor 的用法。应用“ activitymonitor”中就只有一个超链接,单击它会打开谷歌的首页,如代码 清单 3-4 所示。 代码清单 3-4 Instrumentation.ActivityMonitor 使用示例 1. public void test 单击链接 () { 2. fi nal Instrumentation inst = getInstrumentation(); 3. IntentFilter intentFilter = new IntentFilter(Intent.ACTION_VIEW); 4. intentFilter.addDataScheme("http"); 5. intentFilter.addCategory(Intent.CATEGORY_BROWSABLE); 6. View link = this.getActivity().fi ndViewById(R.id.link); ◆ 第 3 章 Android 界面自动化白盒测试64 7. ActivityMonitor monitor = inst.addMonitor(intentFilter, null, false); 8. try { 9. assertEquals(0, monitor.getHits()); 10. TouchUtils.clickView(this, link); 11. monitor.waitForActivityWithTimeout(5000); 12. assertEquals(1, monitor.getHits()); 13. } fi nally { 14. inst.removeMonitor(monitor); 15. } 16. } 在代码清单 3-4 里,测试用例的第 2 行首先获取当前待测应用的仪表盘对象。接着在第 3 ~ 5 行创建了一个意图过滤器(Intent Filter),指明测试用例中感兴趣的意图,即要监听的 意图。并且在第 7 行将我们的监听器添加到仪表盘对象的监听列表中。由于没有人单击待测 应用上的超链接,因此在测试代码第 9 行验证监听器不应该监听到任何对打开浏览器的请 求。但是在第 10 行,测试代码显式单击了超链接,打开浏览器访问谷歌的首页,第 12 行 的验证代码断言监听程序应该监听到一次网页浏览的请求。由于启动浏览器打开网页是一个 相对较长的过程,因此在第 11 行告诉监听程序等待满足要求的活动创建成功,最多等待 5 秒钟。第 14 行执行清理操作,注意这里的清理操作是放在 fi nally 块中执行的,而代码清单 3-4 的测试步骤都被包含在 try... fi nally 块中,这样做的目的是即使测试过程当中有任何错 误,也可以执行清理操作,避免影响后续的测试用例。 3.2 使用仪表盘技术编写测试用例 在 2.3 节中提到,在 Android 一个应用的每个界面都是单独的活动,这样一来 Android 应用可以看成一个由活动(Activity)组成的堆栈,每个活动自身由一系列的 UI 元素 组成,并且具有独立的生命周期,因此应用中的每个活动都可以被单独拿来测试。而 ActivityInstrumentationTestCase2 就是用来做这种测试的,它提供了活动级别的操控和获取 GUI 资源的能力。仪表盘测试用例的流程如图 3-1 所示。 当用户在命令行或者从 Eclipse 中运行测试用例时,首先要把测试用例程序和待测应用 部署到测试设备或模拟器上,再通过 InstrumentationTestRunner 这个对象依次执行测试用例 程序中的测试用例,InstrumentationTestRunner 支持很多参数,用来执行一部分的测试用例, 每个测试用例都是通过仪表盘技术来操控待测应用的各个组件实现测试目的。而测试用例和 待测应用是运行在同一个进程的不同线程上。 Android 仪表盘框架是基于 JUnit 的,ActivityInstrumentationTestCase2 是从 JUnit 的核 心类 TestCase 中继承下来的,这样做的好处就是可以复用 JUnit 的 assert 功能来验证由用户 交互和事件引发的 GUI 行为,而且也让有多年 JUnit 编程经验的程序员容易上手。仪表盘测 试框架的各个测试类型与 JUnit 核心类 TestCase 之间的继承结构如图 3-2 所示。 3.2 使用仪表盘技术编写测试用例 ◆ 65 Run As > Android Unit Test Android 操作系统 命令行 Eclipse $ 测试命令 测试结果输出 Android 进程 线程 线程 测试结果输出 InstrumentationTestRunner Instrumentation Instrumentation Android 测试用例 应用组件 内部状态数据 Android 测试用例程序 Android 待测应用 图 3-1 Android 仪表盘测试用例流程 Instrumentation TestRunner InstrumentationTestCase<> <> AndroidTestCase SyncBaseInstrumentation SingleLaunchActivityTestCase ProviderTestCase2 ApplicationTestCase ActivityInstrumentationTestCase2 ActivityUnitTestCase ActivityTestCase junit. framework TestCase android.test 图 3-2 Android 仪表盘测试框架的测试类图 ◆ 第 3 章 Android 界面自动化白盒测试66 从图 3-2 中可以看到,基本所有的测试用例都是通过 InstrumenationTestRunner 执行的,而 各个测试类型被设计来执行特定的测试,在本章中,主要讲解 ActivityInstrumentationTestCase2 的用法,其他的类型将放在后续章节里讲解。ActivityInstrumentationTestCase2 这个类型是用来针 对单个活动执行功能测试。它通过 InstrumentationTestCase.launchActivity 函数使用系统 API 来创 建待测活动,可以在这个测试用例里直接操控活动,在待测应用的 UI 线程上执行测试函数,也 允许我们向待测应用注入一个自定义的意图对象。 3.2.1 ActivityInstrumentationTestCase2 测试用例 一个 ActivityInstrumentationTestCase2 的测试用例源码框架一般如代码清单 3-5 所示。 代码清单 3-5 ActivityInstrumentationTestCase2 测试用例的源码框架 1. public class SpinnerActivityTest extends 2. ActivityInstrumentationTestCase2{ 3. publicSpinnerActivityTest() { 4. super(SpinnerActivity.class); 5. } 6. 7. @Override 8. protected void setUp() throws Exception { 9. super.setUp(); 10. // 添加自定义的初始化逻辑 11. } 12. 13. @Override 14. protected void tearDown() throws Exception { 15. super.tearDown(); 16. } 17. 18. public void test 测试用例 () throws Exception { 19. // ... 20. } 21. } ActivityInstrumentationTestCase2 泛型类的参数类型是 MainActivity,这样就指定了测试 用例的待测活动,而且它只有一个构造函数——需要一个待测活动类型才能创建测试用例, 其函数声明如下: ActivityInstrumentationTestCase2(Class activityClass) 传递的活动类型应该跟泛型类参数保持一致,代码清单 3-5 的第 1 ~ 5 行就演示了这 个要求。 在 Android SDK 的示例工程“ SpinnerTest”中,有一个很完整的 ActivityInstrumenta- tionTestCase2 测试用例的示例,演示了 Android 仪表盘测试用例的一些最佳实践,如代码清 单 3-6 所示。为了方便读者阅读,笔者将其中的注释用中文翻译过来。 3.2 使用仪表盘技术编写测试用例 ◆ 67 在启动待测活动之前,先将触控模式禁用,以便控件能接收到键盘消息,如代码清 ❏ 单 3-6 的第 54 行。这是因为在 Android 系统里,如果打开触控模式,有些控件是不 能通过代码的方式设置输入焦点的,手指戳到一个控件后该控件自然而然就获取到 输入焦点了,例如戳一个按钮除了导致其获取输入焦点以外,还触发了其单击事件。 而如果设备不支持触摸屏,例如老式的手机,需要先用方向键导航到按钮控件使其 高亮显示,然后再按主键来触发单击事件。在 Android 系统中,出于多种因素的考 虑,在触控模式下,除了文本编辑框等特殊的控件,可触控的控件如按钮、下拉框 等无法设置其具有输入焦点。这样在自动化测试时,就会导致一个严重的问题,因 为无法设置输入焦点,在发送按键消息时,就没办法知道哪个控件最终会接收到这 些按键消息,一个简单的方案就是,在测试执行之前,强制待测应用退出触控模式。 这样在 93 行,我们才能在代码中设置具有输入焦点的控件。 在测试集合中,应该有一个测试用例验证待测活动是否正常初始化,如 69 ❏ ~ 78 行之 间的 testPreconditions 函数。 对界面元素的操作必须放在 UI 线程中执行,如 90 ❏ ~ 97 行的代码块。 代码清单 3-6 Android 示例工程 SpinnerTest 里的最佳实践 1. package com.android.example.spinner.test; 2. 3. import com.android.example.spinner.SpinnerActivity; 4. 5. import android.test.ActivityInstrumentationTestCase2; 6. import android.view.KeyEvent; 7. import android.widget.Spinner; 8. import android.widget.SpinnerAdapter; 9. import android.widget.TextView; 10. 11. public class SpinnerActivityTest 12. extends ActivityInstrumentationTestCase2 { 13. // 下拉框选项数组 mLocalAdapter 中的元素个数 14. public static fi nal int ADAPTER_COUNT = 9; 15. 16. // Saturn 这个字符串在下拉框选项数组 mLocalAdapter 的位置 ( 从 0 开始计算 ) 17. public static fi nal int TEST_POSITION = 5; 18. 19. // 下拉框的初始位置应该是 0 20. public static fi nal int INITIAL_POSITION = 0; 21. 22. // 待测活动的引用 23. private SpinnerActivity mActivity; 24. 25. // 待测活动上下拉框当前显示的文本 26. private String mSelection; 27. 28. // 下拉框当前的选择的位置 ◆ 第 3 章 Android 界面自动化白盒测试68 29. private int mPos; 30. 31. // 待测活动里的下拉框对象的引用,通过仪表盘 API 来操作 32. private Spinner mSpinner; 33. 34. // 待测活动里下拉框的数据来源对象 35. private SpinnerAdapter mPlanetData; 36. 37. /* 38. * 创建测试用例对象的构造函数,必须在构造函数里调用基类 39. * ActivityInstrumentationTestCase2 的构造函数,传入 40. * 待测活动的类型以便系统到时可以启动活动 41. */ 42. public SpinnerActivityTest() { 43. super(SpinnerActivity.class); 44. } 45. 46. @Override 47. protected void setUp() throws Exception { 48. // JUnit 要求 TestCase 子类的 setUp 函数必须 49. // 调用基类的 setUp 函数 50. super.setUp(); 51. 52. // 关闭待测应用的触控模式,以便向下拉框发送按键消息 53. // 这个操作必须在 getActivity() 之前调用 54. setActivityInitialTouchMode(false); 55. 56. // 启动待测应用并打开待测活动。 57. mActivity = getActivity(); 58. 59. // 获取待测活动里的下拉框对象,这样也可以确保待测活动 60. // 正确初始化 61. mSpinner = (Spinner)mActivity.fi ndViewById( 62. com.android.example.spinner.R.id.Spinner01); 63. mPlanetData = mSpinner.getAdapter(); 64. } 65. 66. // 测试待测应用的一些关键对象的初始值,以此确保待测应用 67. // 的状态在测试过程中是有意义的,如果这个测试用例(函数) 68. // 失败了,基本上可以忽略其他测试用例的测试结果 69. public void testPreconditions() { 70. // 确保待测下拉框的选择元素的回调函数被正确设置 71. assertTrue(mSpinner.getOnItemSelectedListener() != null); 72. 73. // 验证下拉框的选项数据初始化正常 74. assertTrue(mPlanetData != null); 75. 76. // 并验证下拉框的选项数据的元素个数是正确的 77. assertEquals(mPlanetData.getCount(), ADAPTER_COUNT); 78. } 3.2 使用仪表盘技术编写测试用例 ◆ 69 79. 80. // 通过向待测活动的界面发送按键消息,在验证下拉框的状态 81. // 是否与期望的一致 82. public void testSpinnerUI() { 83. // 设置待测下拉框控件具有输入焦点,并设置它的初始位置。 84. // 因为这段代码需要操作界面上的控件,因此需要运行在 85. // 待测应用的线程中,而不是测试用例线程中 86. // 87. // 只需要将要在 UI 线程上执行的代码作为参数传入 runOnUiThread 88. // 函数里就可以了,代码块是放在 Runnable 匿名对象 89. // 的 run() 函数里 90. mActivity.runOnUiThread( 91. new Runnable() { 92. public void run() { 93. mSpinner.requestFocus(); 94. mSpinner.setSelection(INITIAL_POSITION); 95. } 96. } 97. ); 98. 99. // 使用手机物理键盘上方向键的主键激活下拉框 100. this.sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); 101. 102. // 向下拉框发送 5 次向“下”按键消息 103. // 即高亮显示下拉框的第 5 个元素 104. for (int i = 1; i <= TEST_POSITION; i++) { 105. this.sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); 106. } 107. 108. // 选择下拉框当前高亮的元素 109. this.sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); 110. 111. // 获取被选元素的位置 112. mPos = mSpinner.getSelectedItemPosition(); 113. 114. // 从下拉框的选项数组 mLocalAdapter 中获取被选元素的数据 115. // (是一个字符串对象) 116. mSelection = (String)mSpinner.getItemAtPosition(mPos); 117. 118. // 获取界面上显示下拉框被选元素的文本框对象 119. TextView resultView = (TextView) mActivity.fi ndViewById( 120. com.android.example.spinner.R.id.SpinnerResult); 121. 122. // 获取文本框的当前文本 123. String resultText = (String) resultView.getText(); 124. 125. // 验证下拉框显示的值的确是被选的元素 126. assertEquals(resultText,mSelection); 127. } 128. } ◆ 第 3 章 Android 界面自动化白盒测试70 3.2.2 sendKeys 和 sendRepeatedKeys 函数 在 Android UI 自动化测试当中,经常需要向界面发送键盘消息模拟用户输入文本,高 亮选择控件之类的交互操作,因此 Android 在 InstrumentationTestCase 类里提供了两个辅 助函数(包括重载)sendKeys 和 sendRepeatedKeys 来发送键盘消息。在发送消息之前, 一般需要保证接收键盘消息的控件具有输入焦点,这可以在获取控件的引用之后,调用 requestFocus 函数实现,如代码清单 3-6 的第 93 行。 sendKeys 的一个重载函数接受整型的按键值作为参数,这些按键值的整数定义在 KeyEvent 类里定义,在测试用例里可以用它向具有输入焦点的控件输入单个按键消息,它 的用法可参考代码清单 3-6 里的第 100 和 105 行。 但 sendKeys 也有一个接受字符串参数的重载函数,它只需要一行代码就可以输入完整 的字符串,字符串里的每个字符以空格分隔,每一个按键都对应 KeyEvent 中的定义,只不 过需要去掉前缀。代码清单 3-7 就演示了两个函数的区别: 代码清单 3-7 sendKeys 和 sendRepeatedKeys 的用法 1. private BookEditor _activity; 2. public void test 编辑书籍信息 () throws Throwable { 3. // 在标题文本框里输入 Moonlight! 4. // 找到“标题”文本框 5. fi nal EditText txtTitle = (EditText) _activity.fi ndViewById( 6. R.id.title); 7. this.runTestOnUiThread(new Runnable() { 8. public void run() { 9. // 通过 AndroidAPI 调用将“标题”文本框 10. // 的文本清空 11. txtTitle.setText(""); 12. // 设置“标题”文本框具有输入焦点 13. txtTitle.requestFocus(); 14. } 15. }); 16. 17. // 依次输入“Moonlight!”的各个按键 18. // 输入一个大写的 "M" 19. sendKeys(KeyEvent.KEYCODE_SHIFT_LEFT); 20. sendKeys(KeyEvent.KEYCODE_M); 21. // 再输入其他小写的字符 22. sendKeys(KeyEvent.KEYCODE_O); 23. sendKeys(KeyEvent.KEYCODE_O); 24. sendKeys(KeyEvent.KEYCODE_N); 25. sendKeys(KeyEvent.KEYCODE_L); 26. sendKeys(KeyEvent.KEYCODE_I); 27. sendKeys(KeyEvent.KEYCODE_G); 28. sendKeys(KeyEvent.KEYCODE_H); 29. sendKeys(KeyEvent.KEYCODE_T); 30. // “!”需要使用虚拟键盘上类似 shift 的按键转义 31. sendKeys(KeyEvent.KEYCODE_ALT_LEFT); 3.2 使用仪表盘技术编写测试用例 ◆ 71 32. sendKeys(KeyEvent.KEYCODE_1); 33. // 关闭虚拟键盘 34. sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); 35. 36. // 验证 " 标题 " 文本框里的内容是期望值 37. String expected = "Moonlight!"; 38. // 因为只是获取控件上的信息,而不是修改,可以直接 39. // 从测试用例线程访问,无需放到 UI 线程中执行 40. String actual = txtTitle.getText().toString(); 41. assertEquals(expected, actual); 42. 43. // 找到“作者”文本框 44. fi nal EditText txtAuthor = (EditText) _activity.fi ndViewById( 45. R.id.author); 46. // 设置 " 作者 " 文本框具有输入焦点 47. this.runTestOnUiThread(new Runnable() { 48. public void run() { 49. txtAuthor.requestFocus(); 50. } 51. }); 52. // 向当前具有输入焦点的控件 -“作者”文本框 53. // 发送 20 个 backspace 按键消息,以便清除 54. // " 作者 " 文本框原有的文本 55. sendRepeatedKeys(20, KeyEvent.KEYCODE_DEL); 56. // 用 sendKeys 字符串重载函数输入“Moonlight!” 57. sendKeys("SHIFT_LEFT M 2*O N L I G H T ALT_LEFT 1 DPAD_DOWN"); 58. assertEquals(expected, txtAuthor.getText().toString()); 59. 60. // 再演示使用 sendRepeatedKeys 将“作者”文本框 61. // 清空,并输入“Moonlight!” 62. sendRepeatedKeys(20, KeyEvent.KEYCODE_DEL, 63. 1, KeyEvent.KEYCODE_SHIFT_LEFT, 64. 1, KeyEvent.KEYCODE_M, 65. 2, KeyEvent.KEYCODE_O, 66. 1, KeyEvent.KEYCODE_N, 67. 1, KeyEvent.KEYCODE_L, 68. 1, KeyEvent.KEYCODE_I, 69. 1, KeyEvent.KEYCODE_G, 70. 1, KeyEvent.KEYCODE_H, 71. 1, KeyEvent.KEYCODE_T, 72. 1, KeyEvent.KEYCODE_ALT_LEFT, 73. 1, KeyEvent.KEYCODE_1, 74. 1, KeyEvent.KEYCODE_DPAD_DOWN); 75. assertEquals(expected, txtAuthor.getText().toString()); 76. 77. // 下面这段代码是不需要的,只是为了暂停 78. // 用例以便观察自动化测试效果所用 79. Thread.sleep(5000); 80. } ◆ 第 3 章 Android 界面自动化白盒测试72 同样是输入一个“ Moonlight!”这个字符串,使用字符串参数的重载版本代码(第 57 行) 要 比 sendKeys 整 型 参 数 的 函 数 版 本(第 19 ~ 34 行) 简 洁 很 多。 第 57 行 演 示 了 sendKeys 的一个技巧,如果要输入重复的字母,只需要在输入的字母前加上要重复 的 次 数 即 可。 第 55 行 和 第 62 行 演 示 了 sendKeys 兄 弟 函 数 sendRepeatedKeys 的 用 法, sendRepeatedKeys 接受一个不定长度的参数列表,其参数两两配对,每对参数的第一个指明 字母重复的次数,第二个就是要输入的字母。第 55 行代码通过发送 20 个回退字符清空文本 框,如果文本框里的字符串长度大于 20 个字符,那么就很有可能导致测试失败,因此在第 11 行笔者又演示了另一种方法清空文本框——直接通过 Android API 显式设置文本框里的文本。 3.2.3 执行仪表盘测试用例 除了通过 Eclipse,还可以在命令行用 Android 系统自带工具 am 执行仪表盘测试用例, 如果不带参数调用,则会执行除性能测试以外的所有测试用例。 $ adb shell am instrument –w < 测试用例信息 > < 测试用例信息 > 的格式一般是“测试用例包名 /android.test.InstrumentationTestRunner”, 例如要执行本章的示例来测试用例,首先需要将其和待测应用安装到设备或模拟器上,在虚 拟机的命令行中输入下面的命令即可执行所有的测试用例: $ adb shell am instrument -w cn.hzbook.android.test.chapter3.test/android.test.InstrumentationTestRunner 测试用例的执行结果直接输出在终端里,如上面命令执行完毕后测试用例的输出结果 如下: # 测试应用中的第一个测试类型 cn.hzbook.android.test.chapter3.test.InstrumentationLimitSampleTest: # 有一个测试用例执行失败,同时输出其堆栈信息 Error in test 添加书籍 : java.lang.NullPointerException at cn.hzbook.android.test.chapter3.test.InstrumentationLimitSampleTest$2.run(Instr umentationLimitSampleTest.java:49) at android.test.InstrumentationTestCase$1.run(InstrumentationTestCase.java:138) at android.app.Instrumentation$SyncRunnable.run(Instrumentation. java:1465) at android.os.Handler.handleCallback(Handler.java:587) at android.os.Handler.dispatchMessage(Handler.java:92) at android.os.Looper.loop(Looper.java:123) at android.app.ActivityThread.main(ActivityThread.java:4627) at java.lang.refl ect.Method.invokeNative(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:626) 3.2 使用仪表盘技术编写测试用例 ◆ 73 at dalvik.system.NativeStart.main(Native Method) # 测试应用中的第二个测试类型 cn.hzbook.android.test.chapter3.test.InstrumentationSampleTest:. # 所有的测试用例都正常运行,没有结果就是好结果 Test results for InstrumentationTestRunner=.E. Time: 11.564 # 总结测试结果,总共运行了两个用例,失败了一个 FAILURES!!! Tests run: 2, Failures: 0, Errors: 1 如果给 InstrumentRunner 指定“ -e func true”这些参数,则会运行所有的功能测试用 例,功能测试用例都是从基类 InstrumentationTestCase 继承而来的。 $ adb shell am instrument –w –e func true< 测试用例信息 > 如果为 InstrumentRunner 指定“-e unit true”这些参数,则会运行所有的单元测试用例, 所有不是从 InstrumentationTestCase 继承的非性能测试用例都是单元测试用例。 $ adb shell am instrument –w –e unit true< 测试用例信息 > 如果为 InstrumentRunner 指定“-e class < 类名 >”这些参数,则会运行指定测试类型里 的所有测试用例。如下面的代码就会运行所有的测试用例: $ adb shell am instrument-w com.android.foo/android.test.InstrumentationTestRunner 执 行 所 有 的 小 型 测 试, 小 型 测 试 用 例 是 指 那 些 在 测 试 函 数 上 标 有 SmallTest 标 签 (annotation)的测试用例: $ adb shell am instrument -w -e size small com.android.foo/android.test.InstrumentationTestRunner 执行所有的中型测试,中型测试用例是那些标有 MediumTest 标签的测试用例: $ adb shell am instrument -w -e size medium com.android.foo/android.test.InstrumentationTestRunner 执行所有的大型测试,大型测试用例是那些标有 LargeTest 标签的测试用例: $ adb shell am instrument -w -e size large com.android.foo/android.test.InstrumentationTestRunner 也 可 只 执 行 具 有 指 定 属 性 的 测 试 用 例, 下 面 是 只 执 行 标 识 有“ com.android.foo. MyAnnotation”的测试用例: $adb shell am instrument-w-e annotation com.android.foo.MyAnnotation com.android.foo/android.test.InstrumentationTestRunner ◆ 第 3 章 Android 界面自动化白盒测试74 指定“ -e notAnnotation”参数来执行所有没有标识有“ com.android.foo.MyAnnotation” 的测试用例: $adb shell am instrument -w -e notAnnotation com.android.foo.MyAnnotation com.android.foo/android.test.InstrumentationTestRunner 如果同时指定了多个选项,那么 instrumentationTestRunner 会执行两个选项指定的测试 集合的并集,例如指定参数“ "-e size large-e annotation com.android.foo.MyAnnotation"”会 同时执行大型测试用例和标识有“com.android.foo.MyAnnotation”的测试用例。 下面的命令执行单个测试用例 testFoo: $ adb shell am instrument-w-e class com.android.foo.FooTest#testFoo com.android.foo/android.test.InstrumentationTestRunner 执行多个测试用例(下例中指定了 com.android.foo.FooTest 和 com.android.foo.TooTest 类型里面的所有测试用例): $ adb shell am instrument-w-e class com.android.foo.FooTest,com.android.foo.TooTest com.android.foo/android.test.InstrumentationTestRunner 只执行一个 Java 包里的测试用例: $ adb shell am instrument-w-e package com.android.foo.subpkg com.android.foo/android.test.InstrumentationTestRunner 执行性能测试: $ adb shell am instrument-w-e perf true com.android.foo/android.test.InstrumentationTestRunner 如果需要调试测试用例,先在代码中设置好断点,然后传入参数“-e debug true”,调试 测试用例的方法会在本书的第二部分讲解。 参 数“ -e log true” 指 明 在“日 志 模 式” 下 执 行 所 有 的 测 试 用 例, 这 个 选 项 会 加 载并遍历其他选项指明的所有测试类型和函数,但并不实际执行它们。它在评估一个 Instrumentation 命令将要执行的测试用例列表时很有用。 如果要获取 EMMA 代码覆盖率,则可以指定“-e coverage true”参数。 提示 这个选项要求应用是一个 emma instrumented 版本,代码覆盖率在后面的章节中会讲到。 3.2.4 仪表盘测试技术的限制 我们知道 Android 应用的每个界面都是一个独立的活动,任意一个活动(界面)都可以 因响应一个意图而创建。而 ActivityInstrumentationTestCase2 原来只是设计用来测试单个活 3.2 使用仪表盘技术编写测试用例 ◆ 75 动的,这在功能测试里往往有很多限制。例如本章示例的待测应用“ cn.hzbook.android.test. chapter3”是一个书籍管理程序,其由 3 个界面组成,要完成一个编辑操作,需要在界面 “ MainActivity”上选择并单击一本书,进入书籍的详细信息界面“ BookDetails”,再单击 “ BookDetails”这个活动或者说界面上的“编辑”按钮进入书籍的编辑界面“ BookEditor”。 在功能测试领域,这是一个非常正常的测试场景,对于信息管理类的应用,这也是一个必须 要测试到的场景,如果使用仪表盘技术,从 ActivityInstrumentationTestCase2 中继承一个新 的测试用例,我们可能会编写类似代码清单 3-8 的用例: 代码清单 3-8 演示 ActivityInstrumentationTestCase2 的限制 1. package cn.hzbook.android.test.chapter3.test; 2. 3. import cn.hzbook.android.test.chapter3.MainActivity; 4. import cn.hzbook.android.test.chapter3.R; 5. import android.test.ActivityInstrumentationTestCase2; 6. import android.widget.Button; 7. 8. public class InstrumentationLimitSampleTest extends 9. ActivityInstrumentationTestCase2{ 10. 11. public InstrumentationLimitSampleTest() throws Exception { 12. super(MainActivity.class); 13. } 14. 15. public void setUp() throws Exception { 16. super.setUp(); 17. setActivityInitialTouchMode(false); 18. } 19. 20. public void tearDown() throws Exception { 21. super.tearDown(); 22. } 23. 24. public void test 添加书籍 () throws Throwable { 25. // 找到 " 添加 " 按钮,根据按钮在源码分配的 id 来查找 26. final Button btnAdd = (Button) getActivity().findViewById(R. id.btnAdd); 27. // 单击按钮需要向控件发送单击消息, 28. // 是需要在 UI 线程上执行的操作 29. this.runTestOnUiThread(new Runnable() { 30. public void run() { 31. btnAdd.performClick(); 32. } 33. }); 34. // 因为测试代码运行速度要比 UI 刷新速度快很多 35. // 需要显示暂停测试代码 500 毫秒等待界面刷新完毕 36. Thread.sleep(500); 37. ◆ 第 3 章 Android 界面自动化白盒测试76 38. // 尝试寻找 " 编辑 " 按钮 39. fi nal Button btnEdit = (Button) getActivity() 40. .fi ndViewById(R.id.btnEdit); 41. this.runTestOnUiThread(new Runnable() { 42. public void run() { 43. // 测试用例会在此失败 44. // 这是因为 " 编辑 " 按钮是 BookDetails 这个活动 45. // 上的控件,而当前用例只能测试 MainActivity 46. // 即 getActivty() 返回的永远都是 MainActivity, 47. // 不可能在其上找到 " 编辑 " 按钮,因此上面的 48. // fi ndViewById 函数返回的是空引用 49. btnEdit.performClick(); 50. } 51. }); 52. 53. fail(" 不应该通过测试 !"); 54. } 55. } 在上面的代码中,在第 31 行单击了“ MainActivity”上的“添加”按钮,这个时候应 用切换到“ BookDetails”界面,而由于 ActivityInstrumentationTestCase2 及其基类没有提供 获取系统当前最上层的活动(即“ BookDetails”)的方法,而 getActivity() 函数永远只返回 传入测试用例构造函数的活动,也就是“ MainActivity”,这样测试用例在第 39 行上尝试在 “ MainActivity”上查找“编辑”按钮时就会失败,进而导致第 49 行因为 btnEdit 是一个空 引用而失败。 如果要对书籍编辑界面“ BookEditor”用仪表盘技术执行功能测试,需要在测试用例 执行之前设置启动“ BookEditor”的意图对象,如代码清单 3-9 第 19 ~ 26 行所示,这些代 码明确设置了启动“ BookEditor”所必需的数据,然后调用 setActivityIntent 函数将意图传 给系统,而且必须在 getActivity() 函数调用之前设置。这样的设计只满足了针对单个活动的 功能测试,但不能支持多活动的集成测试,在下一节,将介绍一个支持集成测试的开源库 robotium 的用法。 代码清单 3-9 在测试用例中使用意图创建待测活动 1. package cn.hzbook.android.test.chapter3.test; 2. 3. import cn.hzbook.android.test.chapter3.BookEditor; 4. import cn.hzbook.android.test.chapter3.R; 5. import android.content.Intent; 6. import android.test.ActivityInstrumentationTestCase2; 7. 8. public class InstrumentationSampleTest extends 9. ActivityInstrumentationTestCase2{ 10. private BookEditor _activity; 11. 12. public InstrumentationSampleTest() throws Exception { 3.3 使用 robotium 编写集成测试用例 ◆ 77 13. super(BookEditor.class); 14. } 15. 16. public void setUp() throws Exception { 17. super.setUp(); 18. setActivityInitialTouchMode(false); 19. Intent i = new Intent(); 20. i.setClass(this.getInstrumentation().getContext(), BookEditor. class); 21. i.setAction(Intent.ACTION_EDIT); 22. i.putExtra("title", " 标题应该被更新 "); 23. i.putExtra("author", " 作者应该被更新 "); 24. i.putExtra("copyright", " 版权所有 "); 25. i.putExtra("indics", 0); 26. setActivityIntent(i); 27. _activity = getActivity(); 28. } 29. 30. public void tearDown() throws Exception { 31. super.tearDown(); 32. } 33. 34. public void test 编辑书籍信息 () throws Throwable { 35. // ... 36. } 37. } 3.3 使用 robotium 编写集成测试用例 开源库 robotium 就是为了弥补 ActivityInstrumenationTestCase2 对集成测试支持的不 足而编写的,其项目主页是:http://code.google.com/p/robotium/,源码已经迁移到 github 上:https://github.com/jayway/robotium, 可 以 从 链 接 http://code.google.com/p/robotium/ downloads/list 处下载最新的 robotium 预编译版本。它除了在仪表盘 API 的基础上提供了更 多的操控控件的函数以外,还通过反射等手段,通过调用系统隐藏的功能,实现仪表盘不能 支持的功能。 3.3.1 为待测程序添加 robotium 用例 要在测试用例里使用 robotium 的 API,首先需要把 robotium-solo-x.x.jar 加入测试用例 工程的引用路径(Build Path)中。 1)将最新下载的 robotium-solo-x.x.jar 保存到测试用例工程根目录的“ libs”文件夹中, 如图 3-3 所示。 2) 在 Eclipse 中 右 键 单 击 测 试 工 程, 并 依 次 选 择“ Build Path”、“ Confi gure Build Path”,如图 3-4 所示。 ◆ 第 3 章 Android 界面自动化白盒测试78 图 3-3 Android 测试用例应用所依赖的 jar 文件的保存位置 图 3-4 打开测试应用的引用路径编辑对话框 3)在弹出的“ Java Build Path”对话框中,选择“ Libraries”标签并单击上面的“ Add External JARs...”按钮,如图 3-5 所示。 图 3-5 添加外部 jar 引用 4)单击“ OK ”确定后,就把对 robotium 的引用添加好了。 使用 robotium 的测试用例代码框架与前文仪表盘用例类似,如代码清单 3-10 所示。 代码清单 3-10 robotium 编写的集成测试用例框架 1. package cn.hzbook.android.test.chapter3.test; 3.3 使用 robotium 编写集成测试用例 ◆ 79 2. 3. import com.jayway.android.robotium.solo.Solo; 4. import android.test.ActivityInstrumentationTestCase2; 5. 6. @SuppressWarnings("rawtypes") 7. public class DemoUnitTest extends ActivityInstrumentationTestCase2 { 8. // 待测试应用的启动主界面类型全名 9. private static String LAUNCHER_ACTIVITY_FULL_CLASSNAME 10. = "cn.hzbook.android.test.chapter3.MainActivity"; 11. // robotium api 主对象 12. private Solo _solo; 13. 14. @SuppressWarnings("unchecked") 15. public DemoUnitTest() throws Exception { 16. super(Class.forName(LAUNCHER_ACTIVITY_FULL_CLASSNAME)); 17. } 18. 19. public void setUp() throws Exception { 20. _solo = new Solo(getInstrumentation(), getActivity()); 21. } 22. 23. public void tearDown() throws Exception { 24. _solo.fi nishOpenedActivities(); 25. } 26. 27. public void test 测试用例 () throws Exception { 28. // ... 29. } 30. } 但此测试用例框架与仪表盘测试用例框架(参照代码清单 3-5)有几点不同。 1)robotium 测试用例虽然也是从 ActivityInstrumentationTestCase2 基类继承下来,但一 般不会使用一个活动类型实例化 ActivityInstrumentationTestCase2 泛型类,如第 7 行。这是 因为 robotium 一般用作集成测试,在一个测试过程中会同时测试到多个活动,只指定一个 活动类型在逻辑上不成立,有时可以用待测应用的主界面来实例化它,但在没有应用源码时 就无法在编译期引入活动类型了。Java 语言建议给泛型类指定一个类型进行实例化,为了规 避这个编译警告,需要在测试类型加上 SuppressWarnings(“rawtypes”) 标签。 2)由于测试类型没有指定待测活动类型,因此在类型的构造函数里,采用反射机制通 过应用主界面的类型名称获取其类型构造测试用例,如代码的第 16 行。 3)在测试的准备函数 setUp 中,一般会通过调用 getInstrumentation() 和 getActivity() 函数获取当前测试的仪表盘对象和待测应用启动的活动对象,并创建 robotium 自动化测试 机器人 solo。跟仪表盘测试用例中的 setUp 函数一样,禁用触控模式、创建启动活动的意图 对象这些操作都应该在 getActivity() 函数之前调用,如第 20 行。 4)因为 robotium 进行的是集成测试,在测试过程中可能会打开多个活动,所以在测试 结束后的扫尾函数 tearDown 中,会调用 robotium API 关闭所有的已打开活动,为后面执行 ◆ 第 3 章 Android 界面自动化白盒测试80 的测试用例恢复测试环境。 robotium 的 API 设计类似后文将要讲解的 selenium 的机器人测试方式,可以将 solo 对 象看成一个机器人,它的每个 API 可以看成机器人可以执行的一个动作,如 waitForView、 searchButton 等,robotium 的 API 名称都采用谓语 + 宾语的方式命名,而且每个 API 都有完 整的注释说明,本书就不再详述各 API 的使用方法。代码清单 3-11 就是针对 3.2.4 小节中 的待测应用执行集成测试的一个示例。 代码清单 3-11 使用 robotium 编写集成测试用例 1. public void test 添加书籍 () throws Exception { 2. _solo.clickOnText(" 添加 "); 3. _solo.sleep(500); 4. _solo.clickOnText(" 编辑 "); 5. 6. // 7. // 在 robotium 中,getEditText 会过滤出所有 EditText 类型的控件 8. // 而 getEditText 函数参数是过滤后 EditText 控件的索引号 9. // 10. 11. // 在标题文本框中输入 Moonlight 12. EditText text = _solo.getEditText(0); 13. _solo.clearEditText(text); 14. _solo.enterText(text, "Moonlight"); 15. 16. // 在作者文本框中输入 David 17. text = _solo.getEditText(1); 18. _solo.clearEditText(text); 19. _solo.enterText(text, "David"); 20. 21. // 在版权文本框中输入出版日期 22. text = _solo.getEditText(2); 23. _solo.clearEditText(text); 24. _solo.enterText(text, "Feb 21, 2011"); 25. 26. _solo.clickOnText(" 保存 "); 27. _solo.clickOnText(" 保存 "); 28. } 从上面的代码可以看到,测试源码类似于手工测试用例的白描,robotium 几乎隐藏了在 测试过程中待测应用各个活动切换的细节。如第 2 和 4 行,给我们的感觉就好像一个机器人 先单击“添加”按钮,等待 0.5 秒,再单击“编辑”按钮,完全不用像代码清单 3-8 里那样 考虑由于不同活动导致查找控件困难的限制。 3.3.2 测试第三方应用 前面的讲解都是基于完全具备源码的前提下进行的测试编码,在很多时候测试团队可 能要在没有待测应用源码的前提下编写自动化测试用例。虽然 Android 提供了另一个工具 3.3 使用 robotium 编写集成测试用例 ◆ 81 monkeyrunner(在第 4 章讲解)来支持黑盒测试 UI 自动化的要求,但相对来说,基于仪表 盘技术的测试用例由于跟待测应用运行在同一个进程里,从效率、准确性以及适应性方面来 说都要比 monkeyrunner 好,所以建议读者尽可能选择仪表盘技术编写测试代码。 要针对第三方应用编写基于仪表盘技术的测试,第一步需要将测试用例注入到待测应用 的进程里,因此要么应用厂商提供调试版本的应用(与测试用例一样使用调试版密钥签名), 要么将应用厂商的发布版本的应用重新打包签名。将应用重新打包签名的一般步骤如下: 1)由于 Android 应用的 apk 安装文件实际上是一个压缩包,可以用解压缩软件将其解压。 2)对于解压后的文件夹下的 META-INF 文件夹,因为其里面包含签名信息,删除它之 后就相当于去掉原有的数字签名。 3)再压缩文件夹并将结果文件的后缀名改为 .apk,重新打包并签名。 在本书配套资源的示例代码中,提供了一个脚本程序 resign.sh 方便重新打包,要重新 打包的 Android 应该需要与该脚本程序放在同一个目录下,它接受一个参数,即要重新打包 的 Android 应用的 apk 文件名(不包括 .apk 后缀),运行完毕后,会在当前的工作目录下生 成一个名为 < 应用名 >-resigned.apk 文件,是重新签名的应用。例如示例代码里有一个来自 于网络的应用 demo.apk,执行下面的命令给其重新签名: $ ./resign.sh demo 注意 resign.sh 假设已经将 Android SDK 根目录添加到程序搜索路径环境变量 PATH 中,如果 没有这么做,执行 resign.sh 程序时可能会报告“zipalign: command not found”的错误。 在命令执行完毕后,就可以看到当前工作目录下有一个名为 demo-resigned.apk 的文件。 代码清单 3-12 解释了这个脚本中的各步操作: 代码清单 3-12 重新签名应用的脚本程序 echo 重新打包 $1.apk # 重新给 product 签名,确保其使用的签名与测试用例的签名一致 # 第一步是删除产品中已有的签名 unzip -o $1.apk -d product cd product # 删除应用已有的签名 rm -r -f META-INF/ # 重新打包应用里的文件 zip -r product.apk * mv product.apk .. cd .. # 删除原来解压用于删除密钥的文件夹——扫尾工作 rm -f -r product # 使用调试用签名重新签名 jarsigner -keystore ~/.android/debug.keystore -storepass android -keypass android product.apk androiddebugkey zipalign 4 product.apk $1-resigned.apk ◆ 第 3 章 Android 界面自动化白盒测试82 在为待测应用重新签名并将其安装到设备或者模拟器上之后,如果运行应用没有发生崩 溃闪退的现象,那么说明重新签名应用已经成功,剩下的就是要找到应用启动的主活动名, 以便传给 ActivitiyInstrumentationTestCase2 的构造函数并启动应用,可以使用 Android SDK 包的 logcat 命令找到这些信息。以刚才重新签名的应用为例: 1)将应用部署到设备或者模拟器上。 2)先启动一次将应用的使用导航和选择城市这些需要第一次设置的信息配置好,按两 次回退键关闭应用。 3)再次启动应用后,在 PC 上执行如代码清单 3-13 所示的这个命令: 代码清单 3-13 通过 logcat 命令找到待测应用的启动界面 $ adb logcat ...... I/ActivityManager( 58): Start proc com.moji.mjweather for activity com.moji. mjweather/.CSplashScreen: pid=962 uid=10046 gids={3003, 1015} D/dalvikvm( 962): GC_FOR_MALLOC freed 3043 objects / 167728 bytes in 97ms I/ActivityManager( 58): Displayed activity com.moji.mjweather/.CSplashScreen: 2288 ms (total 2288 ms) I/ActivityManager( 58): Starting activity: Intent { cmp=com.moji.mjweather/. activity.TabSelectorActivity (has extras) } ...... I/ActivityManager( 58): Displayed activity com.moji.mjweather/.activity. TabSelectorActivity: 3745 ms (total 3745 ms) ...... 注意代码中加粗的部分,其显示了当前应用正在启动“com.moji.mjweather/.CSplashScreen” 这个界面,即应用的初始欢迎界面,反斜杆前面的是应用的包名,后面的是应用当前启动的 活动类名。另外,logcat 输出中的 Displayed activity 后面跟有一个时间,如“2288ms”,这 是创建并显示活动的所消耗的时间,在编写测试用例的时候,可以使用这个信息设置测试代 码等待活动绘制完毕的时间。 4)在 Eclipse 中新建一个 Android 测试工程,并在新建工程向导的选择被测应用界面 上,单击“This project”单选框。 5)然后修改新建测试工程的 AndroidManifest.xml 文件,设置其 instrumentation 节点的 targetPackage 属性值为“com.moji.mjweather”,告诉 Android 系统要测试的应用。如代码清 单 3-14 所示。 代码清单 3-14 测试第三方应用的 AndroidManifest.xml 示例 3.3 使用 robotium 编写集成测试用例 ◆ 83 6)在新建的测试用例中,就可以使用在代码清单 3-13 获取的活动类名进行测试工作 了,如代码清单 3-15 的操作就是等待待测应用的主界面显示之后,单击上面的“趋势”标签: 代码清单 3-15 测试第三方应用的 robotium 测试示例代码 1. package cn.hzbook.android.test.chapter3.test; 2. 3. import com.jayway.android.robotium.solo.Solo; 4. 5. import android.test.ActivityInstrumentationTestCase2; 6. 7. @SuppressWarnings("rawtypes") 8. public class SampleTest extends ActivityInstrumentationTestCase2 { 9. private static String LAUNCHER_ACTIVITY_FULL_CLASSNAME 10. = "com.moji.mjweather.CSplashScreen"; 11. private Solo _solo; 12. 13. @SuppressWarnings("unchecked") 14. public SampleTest() throws Exception { 15. super(Class.forName(LAUNCHER_ACTIVITY_FULL_CLASSNAME)); 16. } 17. 18. public void setUp() throws Exception 19. { 20. _solo = new Solo(this.getInstrumentation(), this.getActivity()); 21. } 22. 23. public void tearDown() throws Exception 24. { 25. _solo.fi nishOpenedActivities(); 26. } 27. 28. public void test 打开主页 () throws InterruptedException { 29. Thread.sleep(3000); 30. _solo.clickOnText(" 趋势 "); 31. Thread.sleep(10 * 1000); 32. } 33. } ◆ 第 3 章 Android 界面自动化白盒测试84 3.3.3 robotium 关键源码解释 为了支持跨活动的集成测试,robotium 在待测应用启动后,使用 ActiveMonitor 每 50 毫 秒监听系统中最新创建的活动,并将其放在内部保留的活动堆栈(模拟 Android 系统里的活 动堆栈)内。因为创建活动的时间比较长,而且一般来说待测应用也不会经常性的创建和销 毁活动(界面),所以 50 毫秒检查一次就足够了。由于 robotium 保存有当前系统内所有的界 面,而且它与待测应用运行在同一个进程里,因此它可以随意查看和在界面里搜索需要的控 件。其关键代码摘抄和批注如代码清单 3-16 所示。 代码清单 3-16 robotium 获取待测应用打开的所有活动的源码批注 1. // 在测试用例的 setUp 函数中 , 一般是调用 Solo 的这个构造函数实例化 Solo, 2. // 其源码保存在 Solo.java 里 3. public Solo(Instrumentation instrumentation, Activity activity) { 4. this.sleeper = new Sleeper(); 5. // ActivityUtils 负责监听和管理活动创建和销毁的消息 6. this.activityUtils = new ActivityUtils(instrumentation, activity, sleeper); 7. this.viewFetcher = new ViewFetcher(activityUtils); 8. // ...... 9. this.clicker = new Clicker(activityUtils, viewFetcher, 10. scroller,robotiumUtils, 11. instrumentation, sleeper, waiter); 12. // ...... 13. } 14. // 下面的源码都保存在 ActivityUtils.java 里 15. public ActivityUtils(Instrumentation inst, Activity activity, Sleeper sleeper) { 16. // ...... 17. 18. // 定时器 , 每个 50 毫秒触发一次 , 判断是否有新活动创建,还是 19. // 有老活动销毁 20. activitySyncTimer = new Timer(); 21. // 保存当前监听到的待测应用的活动堆栈 22. activitiesStoredInActivityStack = new Stack(); 23. // 通过 ActivityMonitor 来监听和管理活动 24. setupActivityMonitor(); 25. setupActivityStackListener(); 26. } 27. 28. /** 29. * This is were the activityMonitor is set up. The monitor will keep check 30. * for the currently active activity. 31. */ 32. private void setupActivityMonitor() { 33. try { 34. // 通过设置 ActivityMonitor 的 IntentFilter 为空 35. // 告诉系统,这个 ActivityMonitor 对所有新创建的 36. // 活动都感兴趣 37. IntentFilter fi lter = null; 3.3 使用 robotium 编写集成测试用例 ◆ 85 38. activityMonitor = inst.addMonitor(fi lter, null, false); 39. } catch (Exception e) { 40. e.printStackTrace(); 41. } 42. } 43. 44. /** 45. * This is were the activityStack listener is set up. The listener will keep track of the 46. * opened activities and their positions. 47. */ 48. private void setupActivityStackListener() { 49. // 设置定时器的回调函数,每隔 50 毫秒调用一次 50. // 通过判断监视待测应用的 ActivityMonitor 获取的最新活动的状态, 51. // 来决定是否新增或删除活动 52. TimerTask activitySyncTimerTask = new TimerTask() { 53. @Override 54. public void run() { 55. if (activityMonitor != null){ 56. Activity activity = activityMonitor.getLastActivity(); 57. if (activity != null){ 58. if(!activitiesStoredInActivityStack.isEmpty() && 59. activitiesStoredInActivityStack.peek().equals( 60. activity.toString())) 61. return; 62. 63. if(!activity.isFinishing()){ 64. if(activitiesStoredInActivityStack.remove(activity.toString())) 65. removeActivityFromStack(activity); 66. 67. addActivityToStack(activity); 68. } 69. } 70. } 71. } 72. }; 73. activitySyncTimer.schedule(activitySyncTimerTask, 0, ACTIVITYSYNCTIME); 74. } 而 robotium 在单击一个控件时,首先会从当前最上层的活动界面取出所有视图,并根 据 API 的要求过滤视图,再获取过滤出来的视图的大小和位置,计算出控件的中点坐标, 最后向 Android 系统注入单击消息(需要包含单击的坐标)来实现单击控件的功能。以本书 经常用到的 clickOnText 函数为例,在代码清单 3-17 中摘抄和批注其关键代码: 代码清单 3-17 robotium clickOnText 的源码批注 1. // Solo.clickOnText 2. // 仅仅是简单地将调用转发到 clicker 对象的 clickOnText 3. // 源码位置:Solo.java ◆ 第 3 章 Android 界面自动化白盒测试86 4. public void clickOnText(String text) { 5. clicker.clickOnText(text, false, 1, true, 0); 6. } 7. 8. // clicker.clickOnText 函数 9. // 可以看到,clickOnText 的第一个参数名,也就是根据文本单击控件的文本参数 10. // 名为 regex,隐含的意思是接受正则表达式 11. // 12. // 源码位置:Clicker.java 13. public void clickOnText(String regex, boolean longClick, int match, boolean scroll, int time) { 14. waiter.waitForText(regex, 0, TIMEOUT, scroll, true); 15. TextView textToClick = null; 16. // 获取待测应用当前活动上所有的视图,或者说控件,因为 17. // Android 里,大部分控件都是从 View 继承下来的 18. ArrayList allTextViews = viewFetcher.getCurrentViews(TextView. class); 19. // 移掉一些不可见的控件,因为不可见的控件是无法单击的, 20. // 这样可以避免一个活动界面上有两个控件具有相同的文本, 21. // 其中一个不可见,导致将单击消息发送到错误的控件上 22. allTextViews = RobotiumUtils.removeInvisibleViews(allTextViews); 23. if (match == 0) { 24. match = 1; 25. } 26. for (TextView textView : allTextViews){ 27. // 针对每个可见的控件,判断其显示的文本与正则表达式相匹配 28. if (RobotiumUtils.checkAndGetMatches( 29. regex, textView, uniqueTextViews) == match) { 30. uniqueTextViews.clear(); 31. textToClick = textView; 32. break; 33. } 34. } 35. // 如果有相匹配的控件,则试图根据控件的大小和位置,计算 36. // 控件的中点坐标,发送单击消息 37. if (textToClick != null) { 38. clickOnScreen(textToClick, longClick, time); 39. // 如果当前的界面是一个列表,而且有滚动条,那么一次向下 40. // 滚动一次,再次查找是否有匹配输入正则表达式的控件。 41. // 这是因为 Android 系统不会为尚未显示的控件分配任何内存 42. // 注意,里面的调用是 clickOnText 函数自己,也就是说这是一个 43. // 递归调用 44. } else if (scroll && scroller.scroll(Scroller.DOWN)) { 45. clickOnText(regex, longClick, match, scroll, time); 46. // 否则没有任何控件上的文本匹配输入的正则表达式, 47. // 只好报错了 48. } else { 49. int sizeOfUniqueTextViews = uniqueTextViews.size(); 50. uniqueTextViews.clear(); 51. if (sizeOfUniqueTextViews > 0) 52. Assert.assertTrue("There are only " + sizeOfUniqueTextViews + 3.4 Android 自动化测试在多种屏幕下的注意事项 ◆ 87 53. " matches of " + regex, false); 54. else { 55. for (TextView textView : allTextViews) { 56. Log.d(LOG_TAG, regex + " not found. Have found: " + 57. textView.getText()); 58. } 59. Assert.assertTrue("The text: " + regex + " is not found!", 60. false); 61. } 62. } 63. } 64. 65. // 这个函数,根据传入控件的大小和位置,计算要单击的中点, 66. // 并向 Android 系统对控件发送单击消息 67. public void clickOnScreen(View view, boolean longClick, int time) { 68. if(view == null) 69. Assert.assertTrue("View is null and can therefore not be clicked!", 70. false); 71. int[] xy = new int[2]; 72. 73. view.getLocationOnScreen(xy); 74. 75. fi nal int viewWidth = view.getWidth(); 76. fi nal int viewHeight = view.getHeight(); 77. fi nal fl oat x = xy[0] + (viewWidth / 2.0f); 78. fl oat y = xy[1] + (viewHeight / 2.0f); 79. 80. if (longClick) 81. clickLongOnScreen(x, y, time); 82. else 83. clickOnScreen(x, y); 84. } 3.4 Android 自动化测试在多种屏幕下的注意事项 在编写 Android 自动化测试用例的时候,可能会碰到这样的情况,在一个 Android 版本 的模拟器上运行得好好的测试用例,在另一个版本的 Android 模拟器上就运行不正常了。基 本症状是,在测试代码中获取一个 View 的实例,然后通过 robotium 的 click 函数单击它: View view = ... // 在代码中获取要单击的 View 的实例 solo.click(view); // 然后单击它 如果是在模拟器上执行,因为创建模拟器的时候可以指定皮肤,模拟器也有不同的版 本,可能会发现在一个皮肤(或者模拟器版本)上运行的好好的,在另一个皮肤(或版本) 上就会发生点不到控件的问题。 发生这种情况,主要是由于 Android 支持多种屏幕造成的,不同屏幕的像素密度可能不 ◆ 第 3 章 Android 界面自动化白盒测试88 一样,这就会导致同样(像素)大小的控件,在低密度屏上看起来要大一些,而在高密度屏 上看起来要小一些,如图 3-6 所示。 图 3-6 不能适应不同像素密度屏幕的控件在各种屏幕上的显示效果 而有些程序,为了避免发生类似上面的情况,会采用密度无关像素的方式指定控件的大 小,即使用 dp 单位。因为 dp 单位采用中等密度屏幕的每英寸的像素个数作为基线,当程序 在高密度或低密度屏上运行时,android 系统会自动据基线来计算并缩放控件,以便相同的 控件在不同密度的屏幕上显示的物理大小是一致的,如图 3-7 所示。 图 3-7 可以适应不同像素密度屏幕的控件在各种屏幕上的显示效果 这样其实就给自动化测试带来了一些问题,在 Android 官方文档中举了一个例子,当然是 开发方面的例子:假如一个程序设置了手指在屏幕上至少移动了 16 个像素才算是滑动,那么 在基准屏上,手指需要移动 16 像素 / 160 dpi,也就是十分之一英寸(或 2.5 毫米);而如果在 高密度屏上面,用户只需要移动 16 像素 / 240 dpi,也就是十五分之一英寸(或 1.7 毫米)。高 密度屏上需要移动的距离远比低密度屏短,给用户的感觉是高密度屏上对手势更敏感些。 在自动化测试的单击上面,针对使用 DPI 指定大小的控件,由于在显示的时候会根 据屏幕的密度来缩放控件,在模拟单击操作的时候,因为 robotium 是复用 instrumentation 类来向 Android 系统发送单击操作这个消息,消息里面自带了单击位置的 x, y 坐标。在 robotium 中单击控件的逻辑是这样的: 1)首先获取要单击的控件 View 的实例。 2)通过 View. getLocationOnScreen 函数获取控件左上角在屏幕上的坐标,坐标的单位 是像素。 3)通过 View.getWidth 和 View.getHeight 函数获取控件的大小。 4)一般来说是单击控件的中间位置,这个位置由控件的左上角的坐标和控件大小计算 得出,这个单位也是像素。 5)原来 robotium 得到单击位置的 x,y 坐标之后,就直接发送 android 消息了,如代码 3.4 Android 自动化测试在多种屏幕下的注意事项 ◆ 89 清单 3-18 所示。 代码清单 3-18 robotium 单击屏幕坐标位置的代码 1. public void clickOnScreen(fl oat x, fl oat y) { 2. long downTime = SystemClock.uptimeMillis(); 3. long eventTime = SystemClock.uptimeMillis(); 4. MotionEvent event = MotionEvent.obtain(downTime, eventTime, 5. MotionEvent.ACTION_DOWN, x, y, 0); 6. MotionEvent event2 = MotionEvent.obtain(downTime, eventTime, 7. MotionEvent.ACTION_UP, x, y, 0); 8. try{ 9. inst.sendPointerSync(event); 10. inst.sendPointerSync(event2); 11. sleeper.sleep(MINISLEEP); 12. }catch(SecurityException e){ 13. Assert.assertTrue("Click can not be completed!", false); 14. } 15. } 由于所有的坐标位置都是以像素计算的,没有考虑到缩放的情形,所以在不同密度的屏 幕上就会发生单击错位的情况。 为了修复这个问题,需要修改 robotium 的源代码更新 Clicker.java 文件中的 clickOnScreen 函数。修改方案是先获取当前屏幕的密度和对 dpi 计算大小的控件的缩放比例,然后恢复原 始的比例再发送单击消息,如代码清单 3-19 所示。 代码清单 3-19 修复 robotium 中的 Clicker.clickOnScreen 函数 1. // 需要传递要单击的控件 View 的实例 2. public void clickOnScreen(View view, boolean longClick, int time) { 3. if(view == null) 4. Assert.assertTrue("View is null and can therefore not be clicked!", false); 5. int[] xy = new int[2]; 6. 7. // 获取控件在屏幕上的位置,如果是 dpi 计算大小的控件,这个位置是缩放后的位置 8. view.getLocationOnScreen(xy); 9. 10. // 获取控件的大小,并且计算出单击的控件中点位置 11. fi nal int top = view.getTop(); 12. fi nal int viewWidth = view.getWidth(); 13. fi nal int viewHeight = view.getHeight(); 14. fl oat x = xy[0] + (viewWidth / 2.0f); 15. fl oat y = xy[1] + (viewHeight / 2.0f); 16. 17. // 计算缩放比例,将要单击的 x, y 坐标恢复到缩放前的情况 18. Activity activity = activityUtils.getCurrentActivity(); 19. DisplayMetrics rdm = activity.getResources().getDisplayMetrics(); 20. DisplayMetrics wdm = new DisplayMetrics(); 21. activity.getWindowManager().getDefaultDisplay().getMetrics(wdm); ◆ 第 3 章 Android 界面自动化白盒测试90 22. x *= wdm.scaledDensity / rdm.scaledDensity; 23. y *= wdm.scaledDensity / rdm.scaledDensity; 24. 25. // 最后再发送 Android 单击消息 26. if (longClick) 27. clickLongOnScreen(x, y, time); 28. else 29. clickOnScreen(x, y); 30. } 最新的 robotium 源码可以从网址 https://github.com/jayway/robotium 下载,代码修改完 成后,在根目录下执行命令“mvn package”就可以编译并打包修改后的代码。 3.5 本章小结 本章主要讲解了使用仪表盘技术和 robotium 开源测试代码针对 Android 应用开展白盒 测试的方法,一般步骤是: 确保测试用例应用和被测应用的签名是一致的,以便 Android 系统将其加载到同一个 ❏ 进程中运行。因此要么使用调试版本的应用,要么对应用进行重新签名。 修改测试用例的 AndroidManifest.xml 文件,在其中添加 Instrumentation 节,并将其 ❏ targetPackage 属性设置为待测应用的包名。 在测试用例中,传入要启动的待测活动的完整类名,开展测试。 ❏ 虽然在中间讲解了使用 robotium 在没有源码的情况下测试第三方应用,但其还是需要 根据 logcat 的输出猜测被测应用的一些内部信息,可以将其看成一个灰盒测试。在下一章, 我们将讲解使用 Android 自带的 monkeyrunner 开展黑盒自动化测试的方法。
还剩100页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

yindonghua

贡献于2016-03-31

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf