Gradle构建系统简介及在Gradle中集成覆盖率工具Jacoco并使用

本文的书写已经是很早之前的事情了,只是把从新排版发了出来,有部分内网资源以及找不到了,有兴趣的同学可以根据原理补充上部分缺失的脚本。

覆盖率统计说明

由于这段很重要,特此在最前面的章节说明。

在多项目工程中(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和生成覆盖率文件,自动化组已经基本解决了问题.

下面讲一讲如何不录制测试脚本,直接手工执行用例。
原理:

  1. jacoco的覆盖率统计开关是在执行测试用例的时候开启的,我们仍然离不开测试工程,但是我们完全可以用一个空白的自动化case来让统计功能开启,然后我们就可以再空白case的执行期间手工操作;
  2. 在\share\membershare\hxiong\DolphinRecordTest\src\free\dante\coverage下有一个TC.java,这就是我们的空白case,它会一直去查找sdcard/jacoco/文件夹里有没有一些特定的文件,找不到则一直sleep然后再找,找到了就会结束掉case;
  3. 所以我们就可以执行该case,通过adb push文件来主动的控制该case的空白时间直到我们需要结束海豚;
  4. 你在执行全功能某一个模块的用例时,需要N次退出海豚,那么你就需要N条这样的空白case;
  5. 请将\share\membershare\hxiong\DolphinRecordTest\src\free\dante\coverage下的DolphinRecordTest工程(为你准备好了的测试工程)拖到你本地,请运行DolphinRecordTest/src/free/dante/coverage/下的initTestcases.py,如果是window系统则直接双击,若是ubuntu则sudo python initTestcases.py,运行后会看到命令行里提示输入一个数,这个数就是你想要的空白case数,记住它也是你能退出海豚的次数限制;
  6. 运行完initTestcases.py后,你会发现在DolphinRecordTest/src/free/dante/coverage/下以TC开头(写死了)的java文件就是N个,然后请将测试工程打包成apk吧;
  7. 当你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的报告长这样的:
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容