Robotium spoon+robotium+jenkins 进行自动化持续回归测试

匿名 · 2015年02月07日 · 最后由 orange 回复于 2016年05月26日 · 2120 次阅读

自动化测试的意义:
别说是外行人,即使是正在从事自动化测试工作的人来说,现在或曾经都或多或少有过这样的疑惑,辛苦写了自动化测试用例,却基本发现不了问题,其意义何在?在说明这个意义前先看下质量的定义。

质量的定义:
维基百科中对于品质(Quality)的定义:中国大陆亦称为 “质量”,可指物品的特征、品性、本质,也可指商品或服务的水准、质量。
影响品质的要素包括物品的可靠性、安全性,功能上是否完备,能否满足需求, 等等。
对于软件质量的定义:软件质量,是指软件系统或系统中的软件部分的质量,即满足用户需求,包括功能需求和性能需求的程度。软件的质量包括功能、性能、可靠性、安全性、可升级性、可维护性、其他质量特性,这也是我们一般认识中的质量,从而也指导着大多数的质量相关的测试工作
从以上的定义中也可以看出,对于质量,大家的认知更多的都是在 “质” 上,而较少地考虑到了 “量” 上,质量质量应该是质与量的结合,既关注 “质” 即品质保证,也要关注 “量” 即效率。随着互联网的高速发展,特别是随着移动互联网的到来,企业希望能够更快地响应市场变化且又希望产品的品质也能够有所保障,因此各类软件项目提速提质相关的如敏捷、持续集成、持续交付、自动化等等技术就诞生了。也就是说对于质量,我们的目的不再仅仅是品质,而是真正的 “质量”,即 Move fast and don’t break things!。
知晓了质量的本质与目的后,再来看前面的问题就好解释多了,自动化测试较手工测试来说,确实远没有手工测试更能发现问题,但正如前文所述质量的意义除了在于 “质” 外,还有 “量”,因此,自动化测试更大的意义在于 “量” 上,在于提高效率上,且实际项目中,自动化也不仅仅只是把测试自动化,但凡能提高效率且适合自动化的均应该进行自动化。当然了,理论上自动化测试还是应该要能发现 20% 左右的问题的,对于一点问题都发现不了的自动化测试,则需要好好思考测试用例设计、测试框架搭建方面的问题了。如何更好、更快、更稳定、更可靠地进行测试自动化,这也即是本文所要分享的。在进入正题前,先看看质量的意义。

质量的意义:
对于用户,质量更多的就意味着软件品质:
试想这么一个例子:某天,大明正在梦乡中与吃着美味的烤鸭,突然被震耳的手机闹铃吵醒了,大明前一晚由于知道明天早上有重要会议,所以特地将手机闹铃调早了半个小时,但也正因为如此,手机闹铃软件出了点故障,比原定时间迟了半小时才响。。大明只好匆匆忙忙顾不上早餐就出门赶去上班了,且赶紧用打车软件叫了辆车,大明心想应该还来得急,就耐心地等着出租车的到来,在瑟瑟寒风转眼间就过去 10 分钟了,大明开始急了,出租车怎么还不来。。又过了 10 分钟,出租车终于出现,原来新手出租车司机由于不认识大明的住处,因此使用了车上的导航,结果由于导航系统未及时更新地图数据,司机开往了错误的地方。。意料之中的,因为没能及时参加会议,大明挨了一顿批。快到中午了,由于心情不佳不想出去吃钣了,大明用外卖软件叫了个外卖,已经饥肠辘辘的大明塞上耳机听着音乐希望可以放松些,结果音乐中间莫名中断过好几次,心情反而更糟了。。。至于下午的事就不说了,最终大明度过了郁闷的一天。
虽然以上的大明略显悲惨,但也决不是没可能发生的,随着移动互联网的到来,每个人的生活越来越需要手机及其上的软件,每个人的幸福度很大程度地受各种软件的影响,如果所有的软件都能很好地运行,这个世界将变得多么美好~

对于企业,质量则将意味着真正的 “质量”:
随着信息的不断爆炸式增长,我们这个时代的节奏也越来越快,特别是移动互联网,一个软件的项目周期再也不能像传统软件那样半年甚至好几年。试想,如果你的软件项目的持续交付能力比竞争对手弱,那么将意味着对方可以更快地响应市场变化、可以比你更快地将创意付诸实现,那么很显然地,你将失去市场,且在移动互联网,在这种情况下往往花费巨大的营销运营代价都无法挽回颓势。因此,提高软件项目的整体效率势在必行,且需要时时刻刻地去思考如何才能加快项目的运转速度、提高持续交付产品的能力并且还能更有效地保障产品的质量。
“让天下人都能用上更好的软件”,想必这应该是质量、测试人员,甚至应该是所有软件相关从业者的任务与使命~
基于以上背景结合实际实践,转入《spoon+robotium+jenkins 进行自动化持续回归测试》正题。。

自动化测试的原因:
例如敏捷测试中所提到的,手动测试需要太长的时间、手动过程容易出错、自动化让人们有时间做更有价值的工作、自动化的回归测试提供了安全网、自动化测试能较早且频繁地提供反馈、测试提供文档等等。
简而言之,自动化测试可以提高测试效率,将重复的活动自动化后可以有更多的时间去做更多有创造性、更需要人脑思考的探索性测试上。
基于 UI 自动化测试的必要性:对于软件,即使编写了单元测试用例、组件测试用例,但这些测试并不是将软件组合成一个整体进行集成测试的,因此很难发现最接近于用户使用的集成测试问题,况且,很多项目团队压根就没有单元测试。对于 android 来说,系统版本碎片化较为严重,编写基于 UI 的自动化测试用例,还可以用来进行兼容性测试。

app 自动化测试应该包含以下几个基本能力:
多机并行执行:同时在多台手机中执行自动化测试用例,以便在不同版本的 Android 操作系统中进行回归测试、兼容性测试
出错重试及出错截图:当用例执行未通过时,可以自动进行重跑并截图记录,以便减少因偶然因素导致用例执行未通过的情况
实时日志记录:对于测试执行过程应该能记录运行时的日志,以便详细发解测试执行情况
跨应用的能力:能够测试包含跨应用的诸多情况
生成 Junit 形式测试报告:生成详细的 Junit 形式的测试报告,可方便查看测试用例执行结果
代码覆盖率报告:生成代码覆盖率报告,以便进一步指导测试策略
持续快速反馈的能力:对于测试运行情况,应该要能够快速反馈
易于访问的报告:能够很方便地访问到测试报告详情

spoon 介绍
项目地址:https://github.com/square/spoon
该项目的主要目的在于将能够将基于 instrumentation 的测试用例分发到各个不同的手机上,执行并将测试结果收集起来,生成最终的 HTML 总结报告。
将项目下载下来后,进入 spoon/website/sample 目录,访问 index.html 即可看到示例如下:

点击查看后有截图展示功能、运行时日志展示功能:


生成报告的目录结构如下:

其中 junit-reports 收集有 junit 格式的 xml 报告,通过 jenkins 的 junit 插件可以很方便生成单元测试报告
简单执行命令如下:

java -jar spoon-runner-1.1.1-jar-with-dependencies.jar \
--apk example-app.apk \
--test-apk example-tests.apk

详细参数:

Options:
--apk Application APK
--fail-on-failure Non-zero exit code on failure
--output Output path
--sdk Path to Android SDK
--test-apk Test application APK
--title Execution title
--class-name Test class name to run (fully-qualified)
--method-name Test method name to run (must also use --class-name)
--no-animations Disable animated gif generation
--size Only run test methods annotated by testSize (small, medium, large)
--adb-timeout Set maximum execution time per test in seconds (10min default)

注:
1.由于 spoon 报告中的静态页面中使用的是 googleapis 中的在线字体,因此报告可能打开会相当慢

因此实际要做为报告时,建议 *** 后访问http://fonts.googleapis.com/cssfamily=Roboto:regular,medium,thin,italic,mediumitalic,boldstatic/fonts.css,然后静态引用,将字体文件下载下来保存至文件例如放至

2.spoon 中未提供开启代码覆盖率的 Option
spoon 由于只是封装了 am instrument 命令,最终执行的其实还是 am instrument 执行用例的命令,因此是可以开启代码覆盖率功能的,要想增加-e coverage true 选项使其支持收集代码覆盖率,则需要修改 spoon 源码,最方便的那就是在源码里写死选项了
SpoonDeviceRunner.java 中增加如下两行写死参数:

logDebug(debug, "About to actually run tests for [%s]", serial);
junitReport = FileUtils.getFile(output, JUNIT_DIR, serial + "_" + i + "_" +".xml");
runner = new RemoteAndroidTestRunner(testPackage, testRunner, device);
runner.setCoverage(true);//注:set为true后,被测应用打包时需要插桩才能生成ec代码覆盖率统计文件
runner.addInstrumentationArg("coverageFile", "/sdcard/robotium/spoon" + i + ".ec");
runner.setMaxtimeToOutputResponse(adbTimeout);

这样,当测试完成后,就会在 sd 卡中生成 emma 用于覆盖率统计的 ec 文件,将 ec 文件 pull 到 CI 服务器上
java -cp /usr/local/program/emma/emma.jar emma report -sp $source_path -r xml -in coverage.em,$ec_files
生成相应的 xml 文件后,结合 jenkins 中的相应插件就可以生成覆盖率报告了。

3.spoon 不能将测试用例集分开执行
adb shell am instrument -w -e class com.android.foo.FooTest,com.android.foo.TooTest com.android.foo/android.test.InstrumentationTestRunner
如上,我们知道通过 am instrument 执行用例时,可以指定多个用例集,当被测试应用启动后,所有接下来要执行的用例集都运行在同一个进程中,如果在执行 FooTest 时,被测应用 crash 了,那么 TooTest 将不再执行,你所收集到的测试结果也就不含 TooTest 的结果了。当你的用例集较多时,显然希望每次回归测试时能尽量把所有用例均执行过,而不希望因前面一个用例 crash 导致后面所有用例都没有执行到。
这时分开执行将类似如下:
adb shell am instrument -w -e class com.android.foo.FooTest com.android.foo/android.test.InstrumentationTestRunner
adb shell am instrument -w -e class com.android.foo.TooTest com.android.foo/android.test.InstrumentationTestRunner
想要让 spoon 支持这种方式运行只能修改 spoon 源码了
对于以上几点不好的地方,博主 fork 了源码做了部分修改,请见:https://github.com/hunterno4/spoon
至此,通过 spoon 即可完成多机并行执行、实时日志记录、生成 Junit 形式测试报告、收集代码覆盖率报告等功能了

出错重试及出错截图
对于自动化测试用例,如果只是执行一次,常常因为偶然因素导致用例不能通过,如果每次收到的测试报告都是因为测试用例执行问题,显然这样的报告很快就不会有人愿意看了。
出错重试一般性做法:
1.收集测试执行时的控制台输出,然后分析输出是否包含某些指定关键字来判断是否失败了,然后进行重跑,这种方式是比较繁琐的。
2.根据 Android junit 自带的@FlakyTest实现的机制,重写 runTest
重写 runTest() 方法可以参考http://qa.baidu.com/blog/?p=985
注:
1) 重写 runTest 时,需要注意的是,测试用例的执行流程如下:
setUp()——> runTest()——> tearDown()
这里的重跑只是重复执行 test*() 方法,因此对于许多跨 Activity 的测试用例,例如从 Activity A 跳转至 Activity B,但用例在 Activity B 中断言失败了,此时重跑时,会继续执行 test***() 方法中点击跳转到 B 的那块代码,但此时界面还处于 B 中,显然是找不到 A 中的那些控件的,因此在重写 runTest() 方法时,需要在 super.runTest() 前自动地跳转到最初的 Activity。这个可以通过 solo.goBackToActivity() 完成
2) 在失败截图时,我们也常常不只希望就只在断言出错时截那一张图,而是希望能截取一系列运行过程的图,这可以通过 solo.startScreenshotSequence() 方法完成
实现方式如下:

@Override
protected void runTest() throws Throwable {

    String testMethodName = getName();
    String currentTestClass = getClass().getName();
    LogUtils.logD(TAG, "currentTestClass:" + currentTestClass);
    boolean isScreenShot = true;
    boolean isScreenShotWhenPass = false;
    long startTime = 0;
    long endTime = 0;
    Holo holo = new Holo(getInstrumentation(), getActivity());//这里的holo是经过增删后的robotium,理解为solo即可
    String currentActivity = getActivity().getClass().getSimpleName();
    LogUtils.logD(TAG, "currentActivity:" + currentActivity);
    Method method = getClass().getMethod(getName(), (Class[]) null);           

    int retrytime = 3;
    if (method.isAnnotationPresent(RetryTest.class)) {
        retrytime = method.getAnnotation(RetryTest.class).retrytime();
        isScreenShot = method.getAnnotation(RetryTest.class).isScreenShot();
    } 
    LogUtils.logD(TAG, "isScreenShot:" + isScreenShot);

    int runCount = 0;

    do {
        LogUtils.logD(TAG, "runCount:" + runCount);
        try {
            holo.goBackToActivity(currentActivity);
            if(runCount > 0){//当用例第一遍执行未通过后,开启截图序列,至于要截多少张图,可以根据实际情况来设计
                holo.stopScreenshotSequence();
                holo.startScreenshotSequence(endTime, 5, testMethodName, currentTestClass);
            }
            startTime = SystemClock.uptimeMillis();
            super.runTest();
            endTime = SystemClock.uptimeMillis() - startTime;
            LogUtils.logD(TAG, "run test" + testMethodName + ",testcase pass with time cost:" + endTime);
            if(isScreenShotWhenPass){
                holo.takeSpoonScreenShot(testMethodName,currentTestClass,testMethodName,DEFAULT_QUALITY);
            }
            if(holo != null){
                holo = null;
            }

            break;              
        } catch (Throwable e) {             
            if(retrytime>1 && runCount<retrytime-1){
                runCount++;
                endTime = SystemClock.uptimeMillis() - startTime;
                LogUtils.logD(TAG, "run test" + testMethodName + ",testcase failed with time cost:" + endTime);
                continue;                   
            }else {
                if(isScreenShot){
                    LogUtils.logD(TAG, "takeScreenshot:");
                    holo.takeSpoonScreenShot(testMethodName,currentTestClass,testMethodName,DEFAULT_QUALITY);
                }   
                if(holo != null){
                    holo = null;
                }

                throw e;
            }

        }

    } while (runCount < retrytime);

}

另外,spoon 本身的截图 client 是不支持截图序列的,因此可以结合 robotium 本身的截图序列的功能,这个只要保证截图的目录是 spoon runner 能理解的即可。

跨应用的能力
对于基于 instrumentation 的框架来说,跨应用能力是其弱势,要想跨应用而又不借助服务端的话,那基本是得 root 手机了。
不过随着 android 系统版本的不断提高,android4.3 及以上很快也就会成为主流,而 4.3 后基于 Uiautomation 就可以从容跨应用了。
另外,对于基于 instrumentation 的框架,我们的测试工程还可以自由调用 android 的 api 来完成 robotium 所不具备的功能,例如完成短信拦截、网络切换、解锁与锁屏等。
以切换网络为例:
当我们实现相关的功能时,可能需要添加用户敏感的权限,如:

而这样的权限可能在我们的被测应用中是没有的,且也不希望在被测应用中加上这样的权限,因此我们只能加到自己的测试工程中。
由于被测应用没有相应的权限,而我们的测试用例是与被测应用运行在同一进程中的,因此要想切换网络的话如果直接调用相关的 api 显然是会报权限问题的,这时我们可以通过把相关的方法封装,然后在测试用例中通过广播的形式来调用。而我们在自己的测试工程里注册接收器,当接收到相应的广播时则调用切换网络的 api,这样的话切换网络相关的 api 则是运行在测试工程这个进程中,而这个进程是有相关权限的。
简而言之,当我们把测试工程.apk 与被测工程.apk 安装到手机上时,我们既可以控制被测工程.apk 这个进程,也可以控制测试工程.apk 这个进程。而在测试工程.apk 这个进程上,我们可以做绝大多数普通 apk 能做的事情,甚至于能做普通 apk 不能做的事情。
例如,我们可以在测试工程.apk 上写个 Activity,然后还可以在测试工程.apk 中写个 testcase 去测试这个 Activity。我们还可以在被测应用自动运行时,在测试工程中启个 service 监控 cpu、内存、流量消耗等性能指标。我们可以默默地连接网络、发送 http 请求,以后通过 Uiautomation 甚至于无需 root 都可以去切换设置。我们几乎无所不能,权大,就是任性。。。

持续快速反馈的能力
测试完成后可以通过邮件反馈,对于 jenkins 的话,可以通过 Email-ext plugin 扩展邮件功能。这个插件也提供很多了邮件模版,可以直接在邮件里展示例如 svn 变更集、junit 报告详情等。在选择要采用的邮件模版前可以先测试下:

注:在 jenkins 中要使用邮件模版的话,需要将插件自带的模版拷贝至你 jenkins 的 workspace 目录下的 email-template(默认不存在,需要自己创建)目录下

易于访问的报告
通过 Spoon 执行后的测试结果默认是收集到了 spoon-output 这个目录下,且可以通过 index.html 静态页面进行访问,因此要想方便地访问每次的测试结果的话,部署个例如 tomcat 这样的 Web 容器就可以了。不过考虑到维护方面且 spoon-output 是静态的,那就完全没必要再弄个容器了,jenkins 有个 userContent 目录,放于这个目录下的所有文件都可以通过 jenkins 直接访问。
因此我们可以把每次测试完成后生成的结果文件自动 copy 到 userContent 下,且按照每次构建的 id 来存放。
例如:
http://192.168.10.111:8080/jenkins/userContent/spoon/162/spoon-output/index.html
这样在每次测试完成后,收到反馈邮件时就可以直接通过邮件中的链接访问测试结果了。

总结:
至此,spoon+robotium+jenkins 进行自动化持续回归测试就可以满足基本的核心需求了,当项目每天 check in 有新代码时,我们通过这些自动化测试用例在多台手机上进行回归测试,测试完成后收到邮件,若有问题时,访问测试结果详情页,根据截图与当时的运行时日志判断是否有 bug,若有的话,即时反馈进行 bug 修复。这样的话,我们即可在多台手机上同时完成回归测试与兼容性测试,手机越多效益越大。当然了,自动化持续回归测试只是提升 “质” 与 “量” 的一部分,对于测试团队来说,接口测试自动化、测试环境运维自动化、性能监控等等等等还有很多。如何提升项目的效率这是永恒的主题,而对于传统的更多地只关注品质的测试团队来说,在新时代下也将越来越难以满足企业对效率的追求,想必这也是 google 工程生产力团队之所以诞生的本质动因。
愿景:对于软件业来说,也许目前的自动化测试仍然还是比较初级的,但正如莱特兄弟发明飞机时,只能飞很小的一段距离,但经过多年几代人不断的付出与努力后,飞行工具已经给人类带来了巨大的效益,即使往更近了看,制造业的自动化已经将众多产业的的流水线变成了自动化,从而大大提高了制造工业的生产力,因此,相信软件业的自动化也将极大地提高软件项目的生产力。

原文:http://blog.csdn.net/hunterno4/article/details/43603959
作者:hunterno4
转载请注明出处~

共收到 20 条回复 时间 点赞

学习了

不错!!学习了!

学习了

先顶再读

@ hunterno4 请问如果有个登录的 APP,是单点登录的,我想 spoon 来跑多机型,怎么处理?

匿名 #6 · 2015年02月09日

#5 楼 @beerbox 把相应的测试用例写好了,然后用 spoon 来执行,执行时保证要跑的这些手机是连接着的就可以了

按照楼主的方法搭建自动化测试平台就差最后一步了,权限问题搞不掂,求指导啊
我把 emma,以及 spoon-runner 都整合到 ant 里面去,在命令行环境执行 ant clean emma debug spoon 可以在 sd 卡生成 coverage.ec,并生成 coverage report。但在 jenkins 中运行 ant 就无法生成 coverage.ec

logcat 如下
I/TestRunner( 8012): finished: testButton2(com.samuel.Botton.test.MainActivityTest)
W/InputManager( 526): Input event injection from pid 8012 permission denied.
I/TestRunner( 8012): passed: testButton2(com.samuel.Botton.test.MainActivityTest)
W/System.err( 8012): java.io.FileNotFoundException: /sdcard/robotium/coverage.ec: open failed: EACCES (Permission denied)
...
W/System.err( 8012): at android.test.InstrumentationTestRunner.generateCoverageReport(InstrumentationTestRunner.java:608)
W/System.err( 8012): at android.test.InstrumentationTestRunner.onStart(InstrumentationTestRunner.java:571)
W/System.err( 8012): at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1584)
W/System.err( 8012): Caused by: libcore.io.ErrnoException: open failed: EACCES (Permission denied)

使用楼主最新的 spoon-runner 测试,已解决权限问题

看不懂

好文,学习了!

不错,学习了。

好文章,收藏了慢慢看,可以加精了

很好的文章,至少可以证明这条路的方向是对的,↖(ω)↗

请问:spoon 修改后的 jar 包哪里可以下载?或者修改后的 spoon 如何编译?

很不错,学习了。之后工作会应用起来

根据楼主的方法已经解决 crash 后不继续执行的问题,但现在有一个问题不太明白,"成后生成的结果文件自动 copy 到 userContent 下,楼主给出的地址http://192.168.10.111:8080/jenkins/userContent/spoon/162/spoon-output/index.html中 spoon 是你的 job 名称吗

好棒的帖子,学习!!

生成的 report 中明明有错误,为什么到了 jenkins 中还是现实 pass 啊?

<?xml version="1.0"?>
-<testsuites>
-<testsuite>
-<testcase time="3.062" casename="testBindService" classname="com.robotium.demo.ServiceTest" ID="1">
<result message="pass">success</result>

</testcase>


-<testcase time="11.719" casename="testError" classname="com.robotium.demo.ServiceTest" ID="2">

<result message=" Button with text: 'Bind Service1222' is not found!">failure</result>

</testcase>


-<testcase time="2.619" casename="testStartService" classname="com.robotium.demo.ServiceTest" ID="3">

<result message="pass">success</result>

</testcase>

</testsuite>

</testsuites>

学习了, 牛逼.~

robotium 脚本中插入 Spoon.screenshot(solo.getCurrentActivity(), "home_listView") 来截屏,直接 run 脚本,出错不能截屏,分析发现是 view.getWidth() 和 view.getHeight() 获取到的值是 0,但 Debug 下调试却可以正常获取屏幕宽高并截屏,不知道楼主有没有遇到这个问题啊?
spoon client version :1.5.3
出错代码行: final Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), ARGB_8888);

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