Android Gradle+CMake+GoogleTest组建Native自动化单元测试

Gradle:[Wikipedia]Gradle is an open-source build-automation system that builds upon the concepts of Apache Ant and Apache Maven and introduces a Groovy-based domain-specific language instead of the XML form used by Apache Maven for declaring the project configuration. Gradle是项目自动化构建的开源工具,相比Maven的XML配置方式,Gradle使用Groovy或Kotlin进行配置,语义简洁而表达力更强,目前已经替代Maven成为主流的项目自动化构建工具。最初Gradle使用Groovy进行脚本编写。虽然Groovy也属于JVM家族,但其动态语言的特性加上Android开发者对其并不熟悉,使得它的编写增加了较多成本。目前Kotlin也支持Gradle脚本编写,Gradle和Kotlin的结合使得Kotlin在Android开发的地位更加突出,Android开发者不但可以使用Kotlin编写业务代码,还可以编写Gradle脚本。Kotlin也给Gradle带来了静态语言的编译期安全等优势。这里不得不提Kotlin与Gradle结合,Jetbrains官网博文Gradle Meets Kotlin, Gradle官网博文Kotlin Meets Gradle Gradle + Kotlin = ️ ️ ,两巨头如恋人般隔空秀恩爱,羡煞旁人。作为软件巨头脑海里迸发出的灵感正在改变世界,能参与其中的软件工程师是何等幸福。

CMake:[Wikipedia]CMake is a cross-platform free and open-source software tool for managing the build process of software using a compiler-independent method. CMake是一个跨平台的编译工具。CMake被推荐为Android Native的编译工具。

GoogleTest:[Wikipedia]Google Test is a unit testing library for the C++ programming language, based on the xUnit architecture. GoogleTest目前被广泛用于C++的单元测试中。

Gradle作为自动化构建工具,对Java、Kotlin的支持非常完备,可以很容易自动化运行单元测试,而对于Native开发单元测试虽然也在尝试支持,但个人并不看好,并且Gradle应该也不会花太大气力。究其原因在于C++语言的依赖不但需要依赖动态链接库,同时还需要使用头文件。C++头文件的放置又不像Java的包有一套标准化的机制;同时考虑到开闭原则,头文件的放置非常灵活。因此Gradle对C++的依赖管理很难控制,CMake作为C++的跨平台编译工具仍会是主流。在Android的Native开发中Gradle+CMake仍将是优选。

Gradle+CMake for Native

在Android Studio中建立一个新的Native C++项目,Android Studio自动会产生默认目录结构:
app/src的目录结构如下:

Demo/
├── app
│   ├── build.gradle
│   └── src
│       ├── androidTest
│       │   └── java
│       │       └── cn
│       │           └── xa
│       │               └── walker
│       │                   └── ExampleInstrumentedTest.kt
│       ├── main
│       │   ├── AndroidManifest.xml
│       │   ├── cpp
│       │   │   ├── CMakeLists.txt
│       │   │   └── native-lib.cpp
│       │   └── java
│       │       └── cn
│       │           └── xa
│       │               └── walker
│       │                   └── MainActivity.kt
│       └── test
│           └── java
│               └── cn
│                   └── xa
│                       └── walker
│                           └── ExampleUnitTest.kt
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── local.properties
└── settings.gradle

通过上图可以看到,Android Studio默认采用Gradle+CMake的方式进行Android Native的工程构建。默认的目录结构非常清晰,在src目录下,可以清晰的看到三个目录。androidTest、main、test。androidTest是需要在安卓设备上跑的用例,test是单元测试用例,main是开发代码区域。在main目录下可以清晰的看到cpp和java部分,而androidTest和test却只有java部分(默认情况下gradle保证每次build时都会执行单元测试的用例,如果用例失败则build失败),而对于Native开发工作者主要的开发集中在cpp侧,如何在工程中添加测试用例来保证代码质量?这将在下一章节阐述。
先把目光拉回工程本身,首先项目根路径下的build.gradle,由于互联网的限制,需要将仓库改为阿里云的仓库。

buildscript {
    ext.kotlin_version = '1.3.20'
    repositories {
        maven { url 'https://maven.aliyun.com/repository/google'}
        maven { url 'https://maven.aliyun.com/repository/gradle-plugin'}
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.3.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'commons-codec:commons-codec:1.6'//此依赖防止出现Base64错误
    }
}

allprojects {
    repositories {
        maven { url 'https://maven.aliyun.com/repository/google'}
        maven { url 'https://maven.aliyun.com/repository/gradle-plugin'}
        maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter'}
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

app目录下的build.gradle文件如下:在该配置可见C++采用cmake的方式编译,并且配置了CMakeLists.txt的路径属性。

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "cn.xa.walker"
        minSdkVersion 23
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++11"
            }
        }
        ndk {
            abiFilter("arm64-v8a")
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0-rc02'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

CMakeLists.txt的作用就是将C++代码编译生成动态链接库,而APP启动主界面时加载该动态链接库。而里面两个externalNativeBuild,这两个externalNativeBuild有何差异?对于疑问最好的问题就是去翻看google的Android的gradle接口文档android-gradle-dsl

android-externalNativeBuild

首先看android下的cmake配置,由于其externalNativeBuild在android的scope里,首先找到index.html找到externalNativeBuild,注意此时的externalNativeBuild类型为ExternalNativeBuild,然后在externalNativeBuild中找到cmake配置,此处可见cmake类型为CmakeOptions.

    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
        }
    }

CMakeOptions的类型定义如下

Property Description
buildStagingDirectory Specifies the path to your external native build output directory.
path Specifies the relative path to your CMakeLists.txt build script.
version The version of CMake that the Android plugin should use when building your CMake project.

defaultConfig-externalNativeBuild

查看defaultConfig下面的cmake配置,由于其defaultConfig在android的scope里,首先找到index.html找到defaultConfig,然后在defaultConfig找到externalNativeBuild,注意此时的externalNativeBuild类型为ExternalNativeBuildOptions(与android的externalNativeBuild类型不同,注意:google的html中defaultConfig页面左侧的externalNativeBuild的链接错误),在ExternalNativeBuildOptions中找到cmake配置,此处可见cmake类型为ExternalNativeCmakeOptions类型.

        externalNativeBuild {
            cmake {
                cppFlags "-std=c++11"
            }
        }

ExternalNativeCmakeOptions的类型定义如下

Property Description
abiFilters Specifies the Application Binary Interfaces (ABI) that Gradle should build outputs for. The ABIs that Gradle packages into your APK are determined by NdkOptions.abiFilter()
arguments Specifies arguments for CMake.
cFlags Specifies flags for the C compiler.
cppFlags Specifies flags for the C++ compiler.
targets Specifies the library and executable targets from your CMake project that Gradle should build.

Tips:在Gradle中配置CMake的参数被认为不是好的实践,应该将其放置在CMakeLists.txt文件中。

通过上述CMake配置的对比可见,在Gradle脚本中只可以指定一个CMakeLists.txt的path路径,而传递给CMake的arguments可以灵活配置,这就为在工程中添加test提供了方向。

Gradle+CMake+googletest for Native unit test

在实践中对于Native的单元测试,采用ProductFlavor。查看google的android-gradle-dsl中ProductFlavor介绍,Product flavors represent different versions of your project that you expect to co-exist on a single device, the Google Play store, or repository. For example, you can configure 'demo' and 'full' product flavors for your app, and each of those flavors can specify different features, device requirements, resources, and application ID's--while sharing common source code and resources. So, product flavors allow you to output different versions of your project by simply changing only the components and settings that are different between them.
由于CMakeLists.txt路径智能指定一个,因此使用不同的编译选项来区分不同的项目成为可能,一种可行的build.gradle配置如下(只给出android部分):gradle配置了ver正式版本和unitTest单元测试版本,使用BUILD_UNITTEST编译宏传给CMake,在CMakeLists.txt进行正式版本与单元测试版本分离编译。由于单元测试需要统计覆盖率信息,可以在unitTest中加入cppFlags进行gcov的覆盖率统计。

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "cn.xa.walker"
        minSdkVersion 23
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        flavorDimensions "version"
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++11"
            }
        }
        ndk {
            abiFilter("arm64-v8a")
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    productFlavors {
        ver {
            externalNativeBuild {
                cmake {
                    arguments "-DBUILD_UNITTEST=FASLE"
                }
            }
        }
        unitTest {
            externalNativeBuild {
                cmake {
                    cppFlags "-fprofile-arcs -ftest-coverage --coverage -fprofile-instr-generate"
                    arguments "-DBUILD_UNITTEST=TRUE"
                }
            }
        }
    }
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
        }
    }
}

一种可行的实践方式是在app/src/test目录下创建cpp文件夹,用来存放cpp单元测试文件。在app/src/main/cpp/CMakeLists.txt中根据编译宏BUILD_UNITTEST进行编译的分离。googletest作为C++的单元测试常用的工具,可以在单元测试中加入编译依赖或者将gtest的文件放置在app/src/test目录下统一编译。当然较好的选择是gradle中加入googletest的依赖,可以参考gradle的项目native-sample中cpp下ibrary-with-tests进行设置。

Android部署的测试用例

如Java分为test和anroidTest一样,C++测试用例有些需要在安卓机器上运行,此时只需要在productFlavors进行添加即可。在测试执行时,会经常读取输入文件。Android分为两种资源目录,res和assets。res和assets的区别,简单的说res是用于R.id使用的,其目录结构相对固定,而assets是静态文件,用特殊方式读取,可以存放任意目录的文件。对于test的apk可以利用其特定进行文件的打包,一种可行是gradle的android下添加如下信息:

    sourceSets{
        ver {
            assets.srcDirs = ['src/main/assets']
        }
        unitTest {
            assets.srcDirs = ['src/test/assets']
        }
    }

当然打包后的配置数据放置的目录是不定的,可以通过Java层主界面创建时利用AssetManager将其拷贝到固定目录,这样Native层可以保证正常读取。

Android-Native覆盖率统计

C++的代码覆盖率统计可以使用gcov & lcov进行统计,但如果在android运行时需要注意一下几点:

  1. Native调用setenv设置GCOV_PREFIX和GCOV_PREFIX_STRIP来设置gcda文件存放路径
  2. 在安卓环境下跑覆盖率,如果没有形成测试报告,考虑在test执行完毕后加入__gcov_flush()主动形成dcda文件
  3. gcda与编译形成的gcov文件最好与源代码放置位置一致

WalkeR_ZG

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348

推荐阅读更多精彩内容