(3) Android 網羅的単体テスト編 - github・CircleCI連携


(1) Android 網羅的単体テスト編 -> https://qiita.com/hal319/items/a8b1d52ac0cfb36f4cee
(2) 網羅率測定 -> https://qiita.com/hal319/items/5bf3f7c8c3c577ac616f
(3) github・CircleCI統合 -> 現在ココ
(4) SonarQube
(5) AppiumでUI部分実装(作成中)

(1)、(2)が終わってしまえばあとは粛々とクラウド側の設定になるので、慣れている人ならそれほど苦痛なくトントンと進んでいくはずです。さて、CircleCIとなると劇的にググっても回答が見つかりにくくなるのは大変にはなっていく。

当然網羅率測定は一人でプロジェクトをやってるのでなければ、チームで行わなければならない。そうなるとソースコード管理ツール上でsubmitをウオッチし、submitされたらばすべての単体テスト(今場合AppiumではなくJUnitレベル)を行えば、安定した開発ができる。

ソースコード管理ツールだが、gitlab等々のオンプレツールはあるが、クラウドのほうが使いやすいので、今回githubを利用する。またビルド単体テストも、jenkinsという選択肢もあるが、これもクラウドで使いやすいCircleCIがあるので。そちらを使わせていただく。

準備

github

あらかじめgithubに、これまでに作成したソースは上げておく。

https://github.com/halsuguro/simple_unit

CircleCI

CircleCIビルドし、単体テスト(網羅的)を行い、各チェックインごとで品質を担保する。なぜCircleCIかというと
- githubやbitbucketとの相性が非常によい。
- クラウドですべてが完結する

確かにJenkinsと比べ、個々でのpros, consはあるがgithubを選んだ時点でCircleCIというのは順当なツールの選択かと思う。

まずCircleCIとJenkinsの連携をする

連携がうまくいけば以下のような画面がでてくる

setup projectボタンを押すと、以下のような大きめな画面が出てくる。

今回は基本的にはmaster一本でシンプルに説明したいので、マニュアル通り

$> ./circleci/config.yml

を作成する。サンプルは
https://circleci.com/docs/2.0/language-android/
にある。

version: 2
jobs:
  build:
    working_directory: ~/code
    docker:
      - image: circleci/android:api-25-alpha
    environment:
      JVM_OPTS: -Xmx3200m
    steps:
      - checkout
      - restore_cache:
          key: jars-{{ checksum "build.gradle" }}-{{ checksum  "app/build.gradle" }}
#      - run:
#         name: Chmod permissions #if permission for Gradlew Dependencies fail, use this.
#         command: sudo chmod +x ./gradlew
      - run:
          name: Download Dependencies
          command: ./gradlew androidDependencies
      - save_cache:
          paths:
            - ~/.gradle
          key: jars-{{ checksum "build.gradle" }}-{{ checksum  "app/build.gradle" }}
      - run:
          name: Run Tests
          command: ./gradlew lint test
      - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ 
          path: app/build/reports
          destination: reports
      - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/
          path: app/build/test-results
      # See https://circleci.com/docs/2.0/deployment-integrations/ for deploy examples

SDKのバージョンが異なる場合は以下を適切なバージョンに修正

circleci/android:api-25-alpha

すでになんらかのファイルがsubmitと同時に実行するようになっているので、config.ymlをアップデート。うまくいけばsucessになる

テスト結果を確認するにはARTIFACTを選ぶと以下のような画面がでてきて、今回の場合

な画面が出てくるので、自分の書いたテストケースのフォルダーをクリックする。

Code Coverage設定

だんだん長くなったが、ここでやっとCode Coverageが動かす。まずconfig.ymlに追記

version: 2.1
jobs:
  build:
    working_directory: ~/code
    docker:
      - image: circleci/android:api-28
    resource_class: medium
    environment:
      JVM_OPTS: -Xmx5000m
    steps:
      - checkout
      #- restore_cache:
      #    key: jars-{{ checksum "build.gradle" }}-{{ checksum  "app/build.gradle" }}
      #      - run:
      #         name: Chmod permissions #if permission for Gradlew Dependencies fail, use this.
      #         command: sudo chmod +x ./gradlew
      - run: sdkmanager --licenses
      - run: yes | sdkmanager --update || exit 0
      - run:
          command: ./gradlew androidDependencies
          when: always
      #- save_cache:
      #    paths:
      #      - ~/.gradle
      #    key: jars-{{ checksum "build.gradle" }}-{{ checksum  "app/build.gradle" }}
      - run:
          name: Run Unit Tests
          command: ./gradlew clean lint test jacocoTestReport
      - store_artifacts:
          path: app/build
          destination: reports
      - run:
          name: Build apk
          command: |
            ./gradlew :app:assembleDebug
            ./gradlew :app:assembleDebugAndroidTest
      - store_artifacts:
          path: ./app/build/outputs/apk/debug/app-debug.apk
          destination: /apks/app-debug.apk
      - store_artifacts:
          path: ./app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk
          destination: /apks/app-debug-androidTest.apkreports/jacoco

jacoco.gradleを追加

/**
 *
 * [Android開発のテストカバー率取得にはこのツールを使い分けると良いという話 - Qiita]
 * https://qiita.com/keidroid/items/adc4f065b84d8a2cd17a
 *
 * [Code Coverage for Android Testing | Rally Health]
 * https://www.rallyhealth.com/coding/code-coverage-for-android-testing
 *
 **/
buildscript {
    repositories {
        mavenCentral()
    }

}

apply plugin: 'com.hiya.jacoco-android'

jacoco {
    toolVersion = "0.8.4" // specify Jacoco version
}

tasks.withType(Test) {
    jacoco.includeNoLocationClasses = true
}

jacocoAndroidUnitTestReport {
    csv.enabled false
    xml.enabled false
    html.enabled true
}

android.applicationVariants.all { variant ->
    def variantName = variant.name.capitalize() //ex. ProdDebug
    def realVariantName = variant.name //ex. prodDebug

    if (variant.buildType.name != "debug") {
        return
    }

    task("jacoco${variantName}TestReport", type: JacocoReport) {
        // Merge coverage result of unit test and ui test
        dependsOn "create${variantName}CoverageReport"
        dependsOn "test${variantName}UnitTest"

        group = "testing"
        description = "Generate Jacoco coverage reports for ${realVariantName}"

        reports {
            xml.enabled = false
            html.enabled = true
        }

        // Set filter to ignore by file name
        def fileFilter = ['**/R.class',
                          '**/R$*.class',
                          '**/BuildConfig.*',
                          '**/Manifest*.*',
                          'android/**/*.*',
                          'androidx/**/*.*',
                          '**/Lambda$*.class',
                          '**/Lambda.class',
                          '**/*Lambda.class',
                          '**/*Lambda*.class',
                          '**/*Lambda*.*',
                          '**/*Builder.*'
        ]
        def javaDebugTree = fileTree(dir: "${buildDir}/intermediates/javac/${realVariantName}/compile${variantName}JavaWithJavac/classes", excludes: fileFilter)
        def kotlinDebugTree = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${realVariantName}", excludes: fileFilter)

        def mainSrc = "${project.projectDir}/src/main/java"

        getSourceDirectories().setFrom(files([mainSrc]))
        // Handle both Java and Kotlin files
        getClassDirectories().setFrom(files([javaDebugTree, kotlinDebugTree]))
        getExecutionData().setFrom(fileTree(dir: project.projectDir, includes: [
                '**/*.exec',    //JUnit Test(unit test) Result
                '**/*.ec'])     //Espresso(ui test) Test Result
        )
    }
}

このjacoco.gradleとconfig.ymlの記述は残念ながら地獄である。2020/8時点。英語でググっても正しい設定が出てこない、結局はjacocoのgithubを参照したり、なだめすかしながら設定を行った。今後もこのサイトはメンテすると思われるので、もし動かない場合はまずこのgithubをコピーし自分でうごかし、自分のプロジェクトに反映させていだだければと思う。

参考資料

https://qiita.com/gold-kou/items/4c7e62434af455e977c2
https://qiita.com/wrongwrong/items/3d6dd806558bacf30a1b
https://circleci.com/docs/2.0/code-coverage/
https://qiita.com/lnkusu/items/2e4cadf44d1db7af2d44