记一次基于 Robotium 改造的测试实践
1、前言
去年年终复盘,测试这边留了两个 Action:一是自动化工具推广,提高开发可操作性;二是 App 自动化稳定性及推广。如何提高可操作性?如何推广?由此便萌生了要做一个专门的 App。
今年初,我们上线了买家端入口,产品采用大量 H5 开发,随着产品迭代的加快以及开发测试比的增加,老的框架已经无法满足新的挑战:
- 开发自测
- 前端页面变动频繁(目前来看这点还好)
- App 上各类组件的测试
- 获取测试覆盖率
无论如何,对于本篇读者我都默认你已经会了一点 Android 测试开发的基础。本篇不谈论做 UI 自动化的投入产出比,而是对 Android 测试技术的研究和一些想法,希望能给到测试开发们工作上的一些帮助。
2、我们的需求
- 给开发自测用
- 增强 H5 测试能力
- 支持组件测试,但不限于 UI 组件
- 能稳定支持 Api >= 15 以上版本
- 方便扩展其他的能力
其中如何能让开发方便使用是本次改造的核心需求。按我的理解,越是简单的操作方式越是方便,所以需求又具体到:
- 要在真机上脱机运行
- 界面操作越简单越好
所以,最终的界面成了这样:
1 个页面 + 1 个按钮。空白的部分是给结果打印留的位置(虽然有点丑,但简单)。
整体流程如下:
3、实现
先说说工具大致的架构:
解释:
-
Instrumentation: Google 测试框架。Robotium 就是在此之上做的封装。通过它来启动被测应用,从而被测应用和测试代码是出于同一进程中。
-
Service: 监听和控制测试任务的开始和停止。在测试 App 启动时作为 service 启动。
-
Util、Helper、Model、Config: 其中 Helper 提供给 service 调用,作为任务启动的入口。Model 是一些 Javabean。Config 配置类,主要配置被测应用的包名和主 Activity 包名。
-
CrashHandler、Reporter、Receiver: 用于监控 crash、发送邮件报告、接收三方推送消息,在 APP 启动时初始化。
-
testcase: 粗粒度的 UI 测试用例,和一些组件测试用例。
-
BroadCast: 现在是通过广播的方式,在主线程和子线程之间进行通信。在这里就是由主线程向 service 发起任务开始和结束的信号。
-
第三方云推送平台: 通过 CI(持续集成)平台向三方发送任务推送,最终推送到手机上。
下面是主要实现过程。
3.1 框架选择
有了需求就拿需求去套,看目前哪个开源框架满足。比较了下业界比较流行的框架,最后还是选择了 Robotium。首先它是基于 Android 官方 Instrumentation 测试框架上做的封装,支持所有的 Api 版本,其次通过 Js 注入的方式,对 H5 测试更好的支持,并且可以使用 Js 进行功能扩展,而且能够很方便的进行二次开发,在 BAT 自主研发的 Android 测试框架中很多是基于它。
顺便提下为什么不是 UIAutomator?Espresso?答:因为不支持 WebView 哦。
3.2 框架改造
本次主要基于 Robotium 对 Webview 相关的测试能力进行改造,加了一些自定义的 JS 方法,用来满足 WebCache 组件的测试需求。
3.2.1 原理
先说说 Robotium 是如何来操控 Webview 的:
如上图,主要涉及到的类有两个 WebUtils 和 RobotiumWebClient。
简单来讲,过程是这样的:在执行某个方法,如 getWebElements()时,Robotium 先将当前 webview 的 WebChromeClient 替换为自己的 RobotiumWebClient,在 RobotiumWebClient 里面重写了 onJsPrompt(),当 Js 里面有调用到 prompt() 的地方,都会走到这个方法里面,收到内容后再封装成 WebElement。
替换后,getJavaScriptAsString()再拿到 RobotiumWeb.js 的内容通过 webview.loadUrl(“javascript:”) 的方式注入到当前页面中。
所以,从这里能看出来,主要实现的方法都在 RobotiumWeb.js 这个文件里面,我们的目的也是往里面增加方法。
3.2.2 动手
根据需求,本次我们要完成 WebCache 组件的测试。测试用例需要验证每个 H5 页面中的静态资源部分可用性,包括 js、css 和图片,另外还需要对组件性能进行评估,最后,还有机型兼容。
如果是手工测试,需要将我们产品下面所有页面都点一遍,看图片有没有正常加载,操作是否顺利,完了后还要记录页面加载时间、资源个数等数据,然而肯定不是测一遍就完事,肯定是要测多遍取平均值。因此我们要自动化,要改造 RobotiumWeb.js。
与前端同学沟通过,判断当前页面图片是否加载,可以从以下两个方面:
- 加载完成后判断当前图片真实的宽高(naturalWidth、naturalHeight)
- 如果是懒加载图片还需要判断 data-src 和 src 是否相同
因此,我们改动的内容如下:
function allDirtyImgs(){
var imgs = document.querySelectorAll('img');
for (var key in imgs){
try{
if(typeof(imgs[key]) == "number"){// 这个情况过滤
return
}
promptDirtyImg(imgs[key]);
}catch(ignored){}
}
finished();
}
// promptDirtyImg部分 -->
var w = element.naturalWidth;
var h = element.naturalHeight;
var src = element.getAttribute('src');
var da = element.getAttribute('data-src');
if(da ==null && (w == 0 || h == 0) ){
prompt('');
}else if(da != null && url !=null && url != da) {
prompt('');
}
然后,在 WebUtils 类里面封装一个 getAllDirtyImgs() 的方法暴露给 Solo,就能直接调用 Solo 了。
boolean javaScriptWasExecuted = executeJavaScriptFunction("allDirtyImgs();");
获取性能数据需要开发同学配合写到 SD 卡中,再常规测试结束后,在通过访问 Sd 卡来拿到数据。最后清理当前数据,这样保证每次测试都是最新。
3.2.3 免不了的失败重试
对于 UI 测试,总会有不稳定的时候,所以此时失败了重新再跑一遍少不了。
我们在测试基类里面同样加入了重试机制:
@Override
protected void runTest() throws Throwable {
int retryTimes = 3;
while(retryTimes > 0)
{
try{
super.runTest();
break;
} catch (Throwable e)
{
if(retryTimes > 1) {
retryTimes--;
tearDown();
setUp();
continue;
}
else
throw e; //记得抛出异常,否则case永远不会失败
}
}
}
3.3 脱机
刚开始,也和网上大部分人一样,在 App 里面我是通过命令方式来启动测试:
Runtime.getRuntime().exec("am instrument");
但是很快就发现,在 Android4.2(Api 17)以上由于权限问题导致不可运行。最终解决方案调整为调用在 Context 下面的 startInstrumentation (ComponentName className, String profileFile, Bundle arguments) 方法。比如:
ComponentName componentName = new ComponentName(packageName, InstrumentationTestRunnerClassName);
mContext.startInstrumentation(componentName, null, null);
参数:packageName:
测试 App 的包名。InstrumentationTestRunnerClassName:
TestRunner 的类名。
3.3.1 脱机持续集成
以往,我们的 CI(持续集成)构建是基于虚拟机或者有 USB 连接线通过 ADB 的方式进行,那么现在是脱机了如何做到?答案是:PUSH。
我们在客户端上集成了三方平台的云推送,在 CI 环境(如 Jenkins)上,封装了云推的透传 API 接口,大概的构建命令像这样:
java -jar /Users/youzan/apis/AppPushServer.jar -run true -mail true -address xx@xx.com -channelId xxx
-channelId
不传,默认推给所有手机,如果要单点推送,需要知道手机的channelId
。-mail true :
运行结果会通过邮件,以HTML报告的形式发送。
** 这种方式缺陷很明显:** 不能保证手机 100% 收到消息,受网络和软件环境的影响较大。
目前,我们主要使用这种方式来做 H5 页面的回归测试和线上接口的稳定性监控。
最后补充一下,毕竟还是涉及 UI 的测试,考虑到执行效率和稳定,我们最终选用作为持续集成的用例仅占到全部用例的 10% 左右。
4、集成其他工具
4.1 集成 UiAutomator
用过 Robotium 的同学都应该知道,我们的测试应用和被测应用处在同一进程的不同线程中,所以如果有需求是跳其他进程(如:调摄像头、相册、手机通知栏)进行操作,Robotium 自己是做不到的。好在 google 在 uiautomator2.0 以上为我们提供了这样的能力,并且可以直接使用 getInstance() 拿到 device 实例。
UiDevice device;
device = UiDevice.getInstance(getInstrumentation());
值得注意的是: 如果通过上面我们使用的 mContext.startInstrumentation() 去启动测试是拿不到device
实例的,此时就必须是通过执行"am instrument"
命令的方式去启动。
4.2 覆盖率统计
由于我们的开发都是使用 gradle 进行打包,而 gradle 自带了 jacoco 的插件,所以我们在被测应用的 build 中加入
buildTypes {
debug{
testCoverageEnabled = true // 主要是这里
}
}
便开启了统计。
为了生成可读性更好的报告,可以再增加task
命令来指定源码路径和忽略一定规则的 class,如:
def coverageSourceDirs = [
'../app/src/main/java'
]
task jacocoTestReport(type: JacocoReport) {
...
classDirectories = fileTree(
dir: './build/intermediates/classes/debug',
excludes: ['**/R*.class',
'**/*$InjectAdapter.class',
'**/*$ModuleAdapter.class',
'**/*$ViewInjector*.class'
])
sourceDirectories = files(coverageSourceDirs)
executionData = files("$buildDir/outputs/code-coverage/connected/coverage.ec")
}
然后,需要测试应用执行测试结束的时候,在指定路径生成覆盖率文件coverage.ec
,主要调用是:
out = new FileOutputStream(CoverageFilePath(),false);
Object agent = Class.forName("org.jacoco.agent.rt.RT").getMethod("getAgent").invoke(null);
out.write((byte[])agent.getClass().getMethod("getExecutionData", boolean.class).invoke(agent, false));
最后,将coverage.ec
从手机 pull 下来放到被测应用下outputs/code-coverage/connected
下面(注意一定要是这个路径),执行gradle jacocoTestReport
最终生成 html 报告。
5、为了改进
到此,我们这个测试 App 能满足我们 90% 日常测试的需求,可是,要完全当作持续集成来用,还有一些事情要做:
- 自动拉取被测包并静默安装
- 聚合报告(将所有手机上的运行结果汇总后,再邮件通知收件人)
- 对 Robotium 再次封装,满足动态用例管理
- 用例推送执行(测试用例以 json 或 xml 方式推送到手机上执行)
- 基于 Robotium 控件遍历的 monkey 测试
以上就是我在平常工作中从一些想法到最后实施的过程。虽然实现上可能不是最好的,也还有些功能待完善,但希望可以借此起到抛砖引玉的效果,帮助无线测试开发们提升产品的测试效率和提高覆盖率。