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测试
以上就是我在平常工作中从一些想法到最后实施的过程。虽然实现上可能不是最好的,也还有些功能待完善,但希望可以借此起到抛砖引玉的效果,帮助无线测试开发们提升产品的测试效率和提高覆盖率。