本文的书写已经是很早之前的事情了,只是把从新排版发了出来,有部分内网资源以及找不到了,有兴趣的同学可以根据原理补充上部分缺失的脚本。
覆盖率统计说明
由于这段很重要,特此在最前面的章节说明。
在多项目工程中(dolphin和topnews都是多项目工程),按照本文的方法仅能统计主工程的代码覆盖率,无法统计库工程的代码覆盖率,如需统计库工程的覆盖率请参考在多项目工程中统计子工程的覆盖率
Gradle
Gradle简介
Gradle是一个开源的,以 Groovy 语言为基础,面向Java应用为主。基于DSL(领域特定语言)语法的自动化构建工具,提供了强大的,可传递的依赖管理系统,目前的Dolphin和其他衍生产品都是使用Gradle进行编译的,他的官网地址为:http://www.gradle.org
Gradle环境搭建
Gradle的版本在一直不停的更新中,截至发稿日期止,最新的版本是2.3。不同的Gralde版本在个别语法定义上会稍有不同(如代码混淆2.1之前的版本为runProguard true,在之后的版本为minifyEnabled true),在搭建编译环境中最好和项目推荐的版本保持一致。
Gradle环境的搭建很简单,在官网下载对应版本的ZIP文件后解压缩,然后将bin目录加到环境变量中或者将gradle软链接到~/bin下即可
- 将bin目录加到环境变量:用文本编辑器或者VIM等工具打开~/.bashrc文件,在最后加上如下的内容即可
export PATH=<bin目录的绝对路径>:$PATH
- 将gradle软链到~/bin目录下:
ln -s <bin目录下gradle文件的绝对路径> ~/bin/gradle
Gradle脚本简介
以下是一个简单的Gradle脚本:
apply plugin: 'android'
apply plugin: "jacoco"
apply from: "$project.rootDir/DolphinBuild/common.gradle"
gradle.beforeProject { project ->
if (project.file('build.gradle').exists()) {
project.buildscript {
repositories {
maven {
name = "Baina Maven Proxy"
url = "http://mirrors.baina.com:8080/archiva/repository/internal"
}
}
dependencies {
classpath 'com.android.tools.build:gradle:1.0.+'
}
}
}
}
android {
// update sdk version to 21, because some variables of LOLLIPOP are used
compileSdkVersion 21
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
}
instrumentTest.setRoot('$project.rootDir/DolphinRecordTest')
androidTest {
manifest.srcFile "$project.rootDir/DolphinRecordTest/AndroidManifest.xml"
java.srcDir "$project.rootDir/DolphinRecordTest/src"
res.srcDirs "$project.rootDir/DolphinRecordTest/res"
assets.srcDirs "$project.rootDir/DolphinRecordTest/assets"
}
}
defaultConfig {
testApplicationId "mobi.mgeek.TunnyBrowser.test"
testInstrumentationRunner "android.test.InstrumentationTestRunner"
}
buildTypes {
debug {
minifyEnabled true
proguardFile 'proguard-debug.cfg'
testCoverageEnabled true
}
release {
minifyEnabled true
proguardFile 'proguard-release.cfg'
}
}
jacoco {
version "0.7.1.201405082137"
}
}
jacoco {
toolVersion "0.7.1.201405082137"
}
dependencies {
androidTestCompile files("$project.rootDir/DolphinRecordTest/libs/robotium.jar")
}
task jacocoTestReport(type: JacocoReport, dependsOn: "connectedAndroidTest") {
def coverageSourceDirs = [
"src",
]
group = "Reporting"
description = "Generate Jacoco coverage reports after running tests."
reports {
xml.enabled true
html.enabled true
}
classDirectories = fileTree(
dir: "./build/intermediates/classes/debug",
includes: ["com/dolphin/browser/popup/RatingPopup*",
"com/dolphin/browser/popup/PopupManager*",
"mobi/mgeek/TunnyBrowser/BrowserActivity*",
"mobi/mgeek/TunnyBrowser/DeferredTaskManager*"],
excludes: ['**/R.class',
'**/R$*.class',
'**/*$ViewInjector*.*',
'**/BuildConfig.*',
'**/Manifest*.*']
)
sourceDirectories = files(coverageSourceDirs)
additionalSourceDirs = files(coverageSourceDirs)
executionData = fileTree(
dir: "$buildDir/outputs/code-coverage/connected"
)
}
Gradle是基于Groovy的脚本,关于Groovy的具体内容请参考:http://www.groovy-lang.org
接下来我们会对每个代码块做简要的介绍
- apply plugin
应用Gradle插件,这些插件可能是Gradle内置的,如jacoco,也可能是外部引入的,如android - apply from
应用其他的gradle脚本,相当于import - android代码块
android gradle plugin定义了其DSL,具体内容可以参考Google的官方文档 - jacoco代码块
jacoco gradle plugin定义了其DSL,具体内容可以参考Gradle的官方文档 - dependency代码块
定义了工程的外部依赖,androidTestCompile定义了androidTest工程的依赖,compile定义源码工程的依赖。 - task代码块
定义了新的task,gradle的build是以task为单位的,用户可以自定义task,也可以使用预置的task(如assemblDebug即是内置的编译debug版本的task),运行
gradle assemblDebug
即可完成编译debug版本的工作,而我们自定义的这个jacocoTestReport task我们会在后面的Jacoco部分详述
使用Gradle编译测试APK
通常Android项目的测试工程就在项目目录下的test目录下,当然为了不干扰源代码的结构,我们也可以将其放到和项目目录并列的文件夹下,以Dolphin项目为例,我们将测试项目(AndroidRecordTest)放在和主工程DolphinBrowserEN平级的目录下,在上面的介绍中我们知道gradle assemblDebug可以编译APK的debug版本,gradle assemblDebugTest便可以生成工程的测试工程,那么如何让gradle脚本识别出我们的测试工程的源码路径呢,在android代码块中有如下的描述:
android {
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
}
instrumentTest.setRoot('$project.rootDir/DolphinRecordTest')
androidTest {
manifest.srcFile "$project.rootDir/DolphinRecordTest/AndroidManifest.xml"
java.srcDir "$project.rootDir/DolphinRecordTest/src"
res.srcDirs "$project.rootDir/DolphinRecordTest/res"
assets.srcDirs "$project.rootDir/DolphinRecordTest/assets"
}
}
defaultConfig {
testApplicationId "mobi.mgeek.TunnyBrowser.test"
testInstrumentationRunner "android.test.InstrumentationTestRunner"
}
}
- 在sourceSets块中main块中定义了源码工程对应的各种目录,androidTest块则描述了测试工程对应的各种目录(如果测试工程目录为源码工程下的test子目录,可以将$project.rootDir/DolphinRecordTest部分替换成test即可)
- instrumentTest.setRoot定义了测试工程的根目录
- defaultConfig块中定义了测试APK的包名和使用的InstrumentationRunner
我们可以将测试工程的build脚本合入到主工程中,也可以独立的创建测试工程的build.gradle文件将其作为独立的项目单独编译
Jacoco
Jacoco简介
Jacoco是一个开源的测试代码覆盖率的框架,所谓代码覆盖率及在执行手动或自动化用例时同时记录源代码中每一行/没一个分支是否都被执行到了,以此来从一个方面反映测试用例是否覆盖了足够多的逻辑分支,我们引入Jacoco到我们的Dolphin工程中以期帮助手工用例差漏补缺已经检查白盒用例的完备性
Jacoco集成
首先需要集成jacoco plugin:
apply plugin: "jacoco"
之后在android块中声明使用的Jacoco的版本即可,同时在对应的BuildType中开启覆盖率统计
android {
jacoco {
version "0.7.1.201405082137"
}
buildTypes {
debug {
testCoverageEnabled true
}
}
}
这样编译出的Debug版本便被成功插桩可以用于覆盖率的统计了
覆盖率测试的执行
覆盖率测试工具的设计本身是为了检验自动化脚本的覆盖率的,做自动化执行时可以通过选项设置将经过Jacoco插桩的Build包的执行记录记录下来,生成文件,和源码对比后生成覆盖率报告,因此我们先来简单讲讲如何验证自动化脚本的代码覆盖率
- 确保被测源码已经集成了Jacoco并编译其Debug版本
- 编译测试用的自动化脚本(可以使用Eclipse编译,或者按照之前的介绍将其Gradle化后使用Gralde编译)
- 将前面两步生成的APK安装到手机上
- 在手机上执行如下的命令,即可开始运行自动化脚本,并在生成覆盖率文件/sdcard/coverage.ec:
adb shell am instrument -w -e coverage true -e coverageFile /sdcard/coverage.ec mobi.mgeek.TunnyBrowser.test/android.test.InstrumentationTestRunner
其中最后一段为测试目标package和使用的TestRunner,可以使用如下命令查询当前已安装的全部测试APK及其对应的TestRunner:
adb shell pm list instrumentation
当然你也可以指定执行测试类或者TestSuite同样也是-e参数,key为class,value为被测类或TestSuite,甚至可以仅仅测试某个类中的某个方法(如仅需测试特定类的全部方法,不带#及其后的方法名即可):
adb shell am instrument -w -e coverage true -e coverageFile /sdcard/coverage.ec -e class free.dante.coverage.TC#testDemo mobi.mgeek.TunnyBrowser.test/android.test.InstrumentationTestRunner
- 将生成的覆盖率文件(在上面的例子中为/sdcard/coverage.ec)从手机中提取出来,准备后面的报告生成
adb pull /sdcard/coverage.rc
如果我们需要测试的是手工用例的代码覆盖率,我们需要一个小小的HACK手段,由于无法直接实现手动执行的覆盖率文件的生成,我们需要一个自动化脚本的引导文件帮助我们启动Dolphin和生成覆盖率文件,自动化组已经基本解决了问题.
下面讲一讲如何不录制测试脚本,直接手工执行用例。
原理:
- jacoco的覆盖率统计开关是在执行测试用例的时候开启的,我们仍然离不开测试工程,但是我们完全可以用一个空白的自动化case来让统计功能开启,然后我们就可以再空白case的执行期间手工操作;
- 在\share\membershare\hxiong\DolphinRecordTest\src\free\dante\coverage下有一个TC.java,这就是我们的空白case,它会一直去查找sdcard/jacoco/文件夹里有没有一些特定的文件,找不到则一直sleep然后再找,找到了就会结束掉case;
- 所以我们就可以执行该case,通过adb push文件来主动的控制该case的空白时间直到我们需要结束海豚;
- 你在执行全功能某一个模块的用例时,需要N次退出海豚,那么你就需要N条这样的空白case;
- 请将\share\membershare\hxiong\DolphinRecordTest\src\free\dante\coverage下的DolphinRecordTest工程(为你准备好了的测试工程)拖到你本地,请运行DolphinRecordTest/src/free/dante/coverage/下的initTestcases.py,如果是window系统则直接双击,若是ubuntu则sudo python initTestcases.py,运行后会看到命令行里提示输入一个数,这个数就是你想要的空白case数,记住它也是你能退出海豚的次数限制;
- 运行完initTestcases.py后,你会发现在DolphinRecordTest/src/free/dante/coverage/下以TC开头(写死了)的java文件就是N个,然后请将测试工程打包成apk吧;
- 当你adb shell am instrument xxxxxx运行测试工程后,请运行controll.py,它是一个伪控制流程脚本,运行后会在命令行里看到当前是第几个空白case及提示你输入指令来结束掉当前的case进入下一条自动化case,或者结束掉所有的自动化case。对了,这里需要注意,若是在ubuntu上运行该脚本,则不用加sudo,直接python controll.py
Jacoco报告的生成
在通过之前的步骤获取到了覆盖率文件后,我们可以通过一个gradle build来将这些覆盖率文件生成最终的XML和HTML格式的覆盖率报告,如果有多个覆盖率文件,生成报告时会自动Merge所有的执行内容,生成一份报告。
由于需要源码做分析,我们必须在源码工程的gradle.build中添加生成Jacoco报告的task,task内容在上面已经有了,我们再贴一遍并简述一下各个参数及其用法
task jacocoTestReport(type: JacocoReport, dependsOn: "connectedAndroidTest") {
def coverageSourceDirs = [
"src",
]
group = "Reporting"
description = "Generate Jacoco coverage reports after running tests."
reports {
xml.enabled true
html.enabled true
}
classDirectories = fileTree(
dir: "./build/intermediates/classes/debug",
includes: ["com/dolphin/browser/popup/RatingPopup*",
"com/dolphin/browser/popup/PopupManager*",
"mobi/mgeek/TunnyBrowser/BrowserActivity*",
"mobi/mgeek/TunnyBrowser/DeferredTaskManager*"],
excludes: ['**/R.class',
'**/R$*.class',
'**/*$ViewInjector*.*',
'**/BuildConfig.*',
'**/Manifest*.*']
)
sourceDirectories = files(coverageSourceDirs)
additionalSourceDirs = files(coverageSourceDirs)
executionData = fileTree(
dir: "$buildDir/outputs/code-coverage/connected"
)
}
- task的类型为JacocoReport,JacocoReport是Gradle内置的一个类型,该类型的task用于生成Jacoco覆盖率报告
- dependsOn: "connectedAndroidTest":这段是表明该task需要在connectedAndroidTest task完成之后进行,connectedAndroidTest是系统内置的自动运行测试工程的task,会默认在连接到电脑的设备上(有且仅有1台,否则会报错)执行全部的测试方法,如果dependsOn的task执行失败了则不会执行我们定义的task。由于我们使用的是adb命令运行测试脚本,因此不要添加这部分,直接写成task jacocoTestReport(type: JacocoReport){}即可
- reports代码块定义各种报告类型的开关,我们在这里开启了XML和HTML格式的报告输出
- classDirectories代码块定义了生成报告使用的目标文件类,他的参数是一个FileCollection类型,我们可以使用FileTree来定义它,dir为目录名,includes后面为需要在报告中显示的文件,excludes为不需要在报告中显示的文件,如果不带includes及其参数会使用dir下的全部文件,否则需要按照其后的参数进行匹配仅使用符合匹配的文件,如果带excludes参数则会从已被选中的文件中在排除掉匹配其后参数的文件。<color red>目标目录需要使用编译后的class文件,即./build/intermediates/classes/debug下的文件,而不是JAVA源码文件</color>
- executionData代码块定义了要被统计的覆盖率文件的路径,该路径下的全部文件都会被用于覆盖率的计算
- 设置完成后运行该task即可生成Jacoco代码覆盖率报告,报告生成的路径为:./build/reports/jacoco/<task名>,其下有XML和HTML两份报告,HTML的报告长这样的: