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

你可能感兴趣的:(Android Gradle+CMake+GoogleTest组建Native自动化单元测试)