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

有赞测试团队 · 2019年04月12日 · 最后由 xinxi 回复于 2019年04月21日 · 2721 次阅读

无标题

[易木]

记一次基于 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();
}
123456789101112
// 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('');
    }
12345678910

然后,在 WebUtils 类里面封装一个 getAllDirtyImgs() 的方法暴露给 Solo,就能直接调用 Solo 了。

boolean javaScriptWasExecuted = executeJavaScriptFunction("allDirtyImgs();");  
1

获取性能数据需要开发同学配合写到 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永远不会失败
            }
        }
    }
123456789101112131415161718192021

3.3 脱机

刚开始,也和网上大部分人一样,在 App 里面我是通过命令方式来启动测试:

Runtime.getRuntime().exec("am instrument");  
1

但是很快就发现,在 Android4.2(Api 17)以上由于权限问题导致不可运行。最终解决方案调整为调用在 Context 下面的 startInstrumentation (ComponentName className, String profileFile, Bundle arguments) 方法。比如:

ComponentName componentName = new ComponentName(packageName, InstrumentationTestRunnerClassName);
mContext.startInstrumentation(componentName, null, null);  
123

参数: 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  
1

-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());  
12

值得注意的是: 如果通过上面我们使用的 mContext.startInstrumentation() 去启动测试是拿不到device实例的,此时就必须是通过执行"am instrument"命令的方式去启动。

4.2 覆盖率统计

由于我们的开发都是使用 gradle 进行打包,而 gradle 自带了 jacoco 的插件,所以我们在被测应用的 build 中加入

buildTypes {  
        debug{
            testCoverageEnabled = true // 主要是这里
        }
}        
12345

便开启了统计。
为了生成可读性更好的报告,可以再增加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")
}
123456789101112131415

然后,需要测试应用执行测试结束的时候,在指定路径生成覆盖率文件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));  
1234

最后,将coverage.ec从手机 pull 下来放到被测应用下outputs/code-coverage/connected下面(注意一定要是这个路径),执行gradle jacocoTestReport最终生成 html 报告。

5、为了改进

到此,我们这个测试 App 能满足我们 90% 日常测试的需求,可是,要完全当作持续集成来用,还有一些事情要做:

  • 自动拉取被测包并静默安装
  • 聚合报告(将所有手机上的运行结果汇总后,再邮件通知收件人)
  • 对 Robotium 再次封装,满足动态用例管理
  • 用例推送执行(测试用例以 json 或 xml 方式推送到手机上执行)
  • 基于 Robotium 控件遍历的 monkey 测试

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

共收到 6 条回复 时间 点赞

CrashHandler 如何监控到被测 app 的崩溃呢?

espresso 是支持 webview 的啊

另外,用 espresso 吧,丢掉 Robotium,毕竟太老了,android 16 出来的东西,很多东西都过时了

已经不错了,还有改造余地,继续加油

用安卓标准 api 注入 js 的话,好处多多,只是你得改 js 源码了 😀

robotium 操作 js 那块,你要舍弃那种方式,安卓 4.4 以下用它,4.4 以上直接调用 evaluateJavascript API 注入即可,好处就是不会破坏应用的 webview,可靠性高,兼容性高,缺点就是无法查看 js 日志,只有一个返回值

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册