记一次基于Robotium改造的测试实践

1、前言

去年年终复盘,测试这边留了两个Action:一是自动化工具推广,提高开发可操作性;二是App自动化稳定性及推广。如何提高可操作性?如何推广?由此便萌生了要做一个专门的App。

今年初,我们上线了买家端入口,产品采用大量H5开发,随着产品迭代的加快以及开发测试比的增加,老的框架已经无法满足新的挑战:

  • 开发自测
  • 前端页面变动频繁(目前来看这点还好)
  • App上各类组件的测试
  • 获取测试覆盖率

无论如何,对于本篇读者我都默认你已经会了一点Android测试开发的基础。本篇不谈论做UI自动化的投入产出比,而是对Android测试技术的研究和一些想法,希望能给到测试开发们工作上的一些帮助。

2、我们的需求

  • 给开发自测用
  • 增强H5测试能力
  • 支持组件测试,但不限于UI组件
  • 能稳定支持Api >= 15以上版本
  • 方便扩展其他的能力

其中如何能让开发方便使用是本次改造的核心需求。按我的理解,越是简单的操作方式越是方便,所以需求又具体到:

  • 要在真机上脱机运行
  • 界面操作越简单越好

所以,最终的界面成了这样: 安卓手机上的界面

1个页面 + 1个按钮。空白的部分是给结果打印留的位置(虽然有点丑,但简单)。

整体流程如下: enter image description here

3、实现

先说说工具大致的架构:

enter image description here

解释:

  • 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的: enter image description here 如上图,主要涉及到的类有两个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测试

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

欢迎关注我们的公众号