티스토리 뷰

최근 클라이언트팀의 안드로이드 제품에 대한 CI / CD 파이프라인을 구성하면서 우리가 만든 제품에 대한 Unit Test Code Coverage를 알 수 있도록 하자는 요구사항이 있었고, 그것을 실현시키기 위한 조사에 들어갔다.

회사의 안드로이드 프로젝트는 단일 레포지토리, 멀티 모듈 구조를 가지고 있으며, Java와 Kotlin 언어를 모두 사용하고, Gradle 빌드 툴을 사용하고 있다.

구글에 Android Project Code Coverage 측정 방법을 검색하기 시작했고, Gradle 프로젝트에 JaCoCo를 사용하면 측정할 수 있다는 정보를 얻어 회사의 안드로이드 프로젝트에 적용을 시도하였다.

여기서부터 고난과 역경의 시간이 시작되었다.

구글 검색을 통해 얻은 Gradle 프로젝트에 JaCoCo 적용 가이드에 따라 안드로이드 프로젝트에 적용하였을 때, 플러그인을 정상적으로 찾지 못하는 등 문제가 있었고, 안드로이드 프로젝트에 JaCoCo 적용 가이드는 프로젝트에 단일 App, 단일 모듈만 있는 프로젝트에 적용하는 방법을 알려주고 있어 멀티 모듈 구조를 가지고 있는 회사 안드로이드 프로젝트에 적용하기에 무리가 있었다.

또, 찾은 가이드 글은 Out Dated 되었거나, 설명이 잘 안되어있어, 회사 안드로이드 프로젝트 구조에 맞춰 커스텀하기에 많은 어려움이 있었다.

많은 위기와 고난, 역경이 있었지만 나는 결국 안드로이드 멀티 모듈 프로젝트에서 JaCoCo를 이용한 Code Coverage 측정 및 Reports 생성에 성공하였고, 나와 같이 고난과 역경을 겪을 안드로이드 개발자들의 삽질을 막는 한줄기 빛이 되기 위해 이 글을 작성하였다.

이 글은 멀티모듈 구조를 가지고, Java, Kotlin을 모두 사용하는 안드로이드 프로젝트의 코드 커버리지 측정 (via JaCoCo) Gradle Task 설정 및 적용, 사용 방법을 설명한다.

안드로이드 멀티 모듈

하나의 프로젝트에 복수의 모듈을 구성하는 것을 뜻한다
멀티 모듈 구조가 무엇인지 어떤 장점이 있는지는 아래의 글 참고

https://www.youtube.com/watch?v=H4qh0n9Zu5k- 한글!

https://medium.com/google-developer-experts/modularizing-android-applications-9e2d18f244a0

https://www.youtube.com/watch?v=jrnhIgFzgns&feature=emb_title

 

Jacoco

JaCoCo는 Java 및 Kotlin 코드의 커버리지를 체크하는 라이브러리다. 
유닛 테스트를 실행하고 커버리지 결과를 html이나 xml, csv 리포트로 생성한다.
그리고 테스트 결과가 설정한 커버리지 기준을 만족하는지 확인하는 기능을 제공한다.

 

JaCoCo 적용하기

JaCoCo를 이용한 Code Coverage 측정 및 리포트 생성 Gralde Task를 생성하고 각 모듈에 적용하는 방법을 설명한다
아래에 설명한 내용은 Code Coverage Report Sample github repo에서 확인할 수 있다.

1. Root build.gradle에 JaCoCo 설정

최상단 루트 build.gradl에 Jacoco 플러그인을 사용하겠다고 설정한다.

// <rootDir>/build.gradle

buildscript {
	...
}

plugins {
	// Jacoco Plugin 추가
    id 'jacoco'
}

allprojects {
	...
}

2. JaCoCo Custom Gradle Task 추가

Jacoco Report 생성 Gradle Task를 Custom Gradle Task 파일로 작성할 것이다.
<rootDir>/gradle 디렉터리에 jacoco.gradle 파일을 생성하자 (파일 이름은 원하는 대로 작성하시면 됩니다. 확장자는 유지)

3. JaCoCo 버전 설정

사용할 Jacoco의 버전을 설정한다.
최신 버전은 JaCoCo 공식 사이트에서 확인할 수 있다.
이 글을 작성하는 시점에 최신 버전인 0.8.5 버전을 설정한다

0.8.5 버전에는 Kotlin 컴파일러에서 자동으로 생성한 메서드를 필터링하지 못하는 문제가 있어, Kotiln으로 작성된 모듈의 코드 커버리지를 정상적으로 측정하지 못하는 문제가 있다. 이 문제는 다음 버전에서 수정 된다고하니 출시되면 바로 적용하자.
(Ex. Data Class를 컴파일하면 get 메서드가 자동으로 생성되는데, 커버리지 리포트에 해당 메서드의 테스트 커버리지가 0%로 측정됨)

// <rootDir>/gradle/jacoco.gradle

// Jacoco 플러그인을 사용하겠다
apply plugin: 'jacoco'

// 적용할 jacoco의 버전은 0.8.5
jacoco {
    toolVersion = '0.8.5'
}

4. 코드 커버리지 생성 Gradle Task 추가

 JaCoCo를 사용하여 코드 커버리지 리포트를 생성하는 작업을 수행하는 Gradle Task를 추가한다.
이름은 Task가 하는 일을 바로 알 수 있도록 설정하며, 동사가 먼저 나오는 것이 좋다
추가하려는 Task는 Code Coverage Report를 생성하는 Task로, 이름을 'generateCodeCoverageReports'로 작성하였다.

// <rootDir>/gradle/jacoco.gradle

// flavor가 설정된 모듈의 경우, 적용된 flavor 명칭을 알아야한다
// 모듈의 build.gradle 파일에 설정된 flavor 값을 가져온다
String flavor = project.ext.has('pFlavor') ? project.ext.pFlavor : ''

// jacoco는 jutni test 결과를 기반으로 동작한다
// jacoco 리포트 생성 작업 이전에 unit test task가 완료되어야한다 
String dependsOnTask = "test${flavor.capitalize()}DebugUnitTest"

// jacoco gradle task 추가
// jacocoReport 타입의 task이며,
// 위에서 알아낸 dependsOnTask를 종속 task로 설정한다 
task generateCodeCoverageReports(type: JacocoReport, dependsOn: dependsOnTask) {
	group = "Reporting"
    description = "Generate Jaoco Coverage Reports"
}

Jacoco Code Coverage는 Unit Test Result를 기반으로 측정된다.
즉, Jacoco Code Coverage 측정 및 리포트 생성 전에 Unit Test Task가 완료되어야 한다.
테스트 진행 후 커버리지 측정이 이루어질 수 있도록 하기위해, 유닛 테스트를 수행하는 Task를 depend Task로 지정한다.

android 모듈의 일반적인 unit test 수행 Task 이름은 'testDebugUnitTest'로 설정되어있으나
Flavor를 사용하여 여러 빌트 타입을 구성한 모듈의 경우 'test<Flavor이름>DebugUnitTest' 이름으로 설정되어있다
Gradle 플러그인으로 android의 flavor를 알아낼 수 없어 모듈에서 flavor 값을 알려줄 수 있도록 설정해야 한다
5번 줄 pFlavor 파라미터를 설정하는 방법은 아래 '각 모듈에 Code Coverage 생성 via JaCoCo Gradle Task 적용하기' 에서 설명하겠다.

5. 생성할 Code Coverage Report 확장자 타입 및 생성 위치 설정

Jacoco 리포트는 html, xml, csv 확장자 파일로 생성성할 수 있다.
생성하기 원하는 확장자 타입을 설정하고, 어느 위치에 생성할 것인지 설정한다.
일반적으로 html, xml 타입을 사용한다.

// <rootDir>/gradle/jacoco.gradle

task generateCodeCoverageReports(type: JacocoReport, dependsOn: dependsOnTask) {
    
	def reportDirPath = "$buildDir/reports/codeCoverage"

    reports {
        html.enabled true
        xml.enabled true

        html.destination file("$reportDirPath/${project.name}")
        xml.destination file("$reportDirPath/${project.name}.xml")
    }
}

6. 커버리지 측정에서 제외할 파일 설정

커버리지 측정에서 제외할 파일을 설정한다. (안드로이드 시스템 파일 및 Dagger에서 생성된 파일 등)
Dagger가 적용된 모듈은 빌드시 Factory 클래스 등이 자동으로 생성되는데, 자동 생성된 클래스를 제외해야 정확한 커버리지를 측정할 수 있다.

// <rootDir>/gradle/jacoco.gradle

task generateCodeCoverageReports(type: JacocoReport, dependsOn: dependsOnTask) {

    def fileFilter =
            [
                    '**/R.class',
                    '**/R$*.class',
                    '**/BuildConfig.*',
                    '**/Manifest*.*',
                    '**/*Test*.*',
                    'android/**/*.*',
                    '**/*InjectAdapter*.*',
                    '**/*StaticInjection*.*',
                    '**/*ModuleAdapter*.*',
                    // Dagger
                    '**/*_Provide*/**',
                    '**/*_Factory*/**',
                    '**/*_MembersInjector.class',
                    '**/*Dagger*'
            ]

}

7. 커버리지 측정 설정

여기가 가장 중요한 부분이다.
Jacoco는 Java, Kotlin으로 작성된 소스코드와, 그 소스가 컴파일 된 결과물을 비교하여 커버리지를 측정한다.
Gradle 버전에 따라 컴파일 결과물의 경로가 달라지고, Java, Kotlin의 경로 또한 다르게 설정되어있다.

이 사실을 모른체 out dated 된 설정 가이드의 내용을 똑같이 설정하였더니 커버리지가 전혀 측정되지않는 문제가 있었다.
원인을 찾고 컴파일 결과 경로를 올바르게 설정하고난 뒤에야 정상적으로 동작했다.

샘플에 적용된 android Gradle의 버전은 4.0.0으로 다른 Gradle 버전과 컴파일 결과물 경로가 다를 수 있으니 본인의 프로젝트에 설정된 Gradle 버전의 경로를 찾아 (잘) 설정할 수 있도록 하자.

Java, Kotlin을 모두 사용하는 경우, 언어별 각각 설정해야한다! 명심하자!

// <rootDir>/gradle/jacoco.gradle

task generateCodeCoverageReports(type: JacocoReport, dependsOn: dependsOnTask) {

    def javaClassDirPath = "$project.buildDir/intermediates/javac/debug/classes"
    def kotlinClassDirPath = "$project.buildDir/tmp/kotlin-classes/debug"
    def coverageExecutionDataPath = "${buildDir}/jacoco/testDebugUnitTest.exec"

    // flavor가 설정된 경우, 결과물의 경로가 변하기 때문에, 꼭 설정해줘야한다
    if (!flavor.isEmpty()) {
        javaClassDirPath = "$project.buildDir/intermediates/javac/${flavor}Debug/classes"
        kotlinClassDirPath = "$project.buildDir/tmp/kotlin-classes/${flavor}Debug"
        coverageExecutionDataPath = "${buildDir}/jacoco/test${flavor.capitalize()}DebugUnitTest.exec"
    }

    def mainJavaSrcPath = "$project.projectDir/src/main/java"
    def mainKotlinSrcPath = "$project.projectDir/src/main/kotlin"

    sourceDirectories.from = files([mainJavaSrcPath, mainKotlinSrcPath])
    classDirectories.from = fileTree(
            dir: javaClassDirPath,
            excludes: fileFilter
    ) + fileTree(
            dir: kotlinClassDirPath,
            excludes: fileFilter
    )
    executionData.from = file(coverageExecutionDataPath)

}

sourceDirectories : 커버리지를 측정할 소스 디렉터리를 지정합니다.
classDirectories : 소스 디렉터리 내 클래스를 컴파일한 결과인 *.class 파일이 있는 디렉터리를 지정합니다. 위에서 작성한 커버리지 측정 제외 리스트를 설정하여 제외될 수 있도록 합니다. java와 kotlin 컴파일 결과 파일의 위치가 서로 다르며 Gradle 버전에 따라 위치가 변경될 수 있습니다.
executionData : 커버리지 측정 결과를 저장할 파일 이름을 지정합니다.

8. Unit Test 실행 후, 커버리지 측정 Task 수행 설정

유닛 테스트 진행이 완료된 후, 바로 이어서 커버리지 측정이 이루어질 수 있도록 하기위해, 유닛 테스트를 수행 이후 커버리지 측정 Task를 수행하도록 설정한다.

// <rootDir>/gradle/jacoco.gradle

task generateCodeCoverageReports(type: JacocoReport, dependsOn: dependsOnTask) {

}

tasks.all { task ->
    if (task.name.equals(dependsOnTask)) {
        task.finalizedBy generateCodeCoverageReports
    }
}

 

각 모듈에 Code Coverage 생성 via JaCoCo Gradle Task 적용하기

Jacoco Gradle Task 설정이 완료되었다.
이제 각 모듈에 위 Gradle Task를 설정하여 커버리지를 측정해보자

각 모듈의 build.gradle에 jacoco.gradle을 설정한다

// <moduleDir>/build.gradle

android {
	...
}

dependencies {
	...
}

// flavor를 사용하는 경우에 커버리지 측정에 사용할 Flavor 이름을 설정합니다.
ext {
  pFlavor = '<FlavorType>'
}

apply from: "$rootDir/gradle/jacoco.gradle"

 

커버리지 측정 Task 실행

Code Coverage Report를 생성하기 위해서는 1개 이상의 Unit Test Case를 작성해야한다. Unit Test Case가 1개도 없다면 리포트는 생성되지않는다.

아래의 명령어를 통해 커버리지 측정을 실행합니다.

1. 커버리지 측정 Task 실행 - 커버리지 측정 Task 직접 실행

/gradlew :<module>:generateCodeCoverageReports

2. Test Task 실행 - 테스트 진행 후 커버리지 측정이 자동으로 진행된다

./gradlew :<module>:test 
// 혹은
// ./gradlew :<module>:testDebugUnitTest

 

커버리지 측정 결과 확인

커버리지 측정 Task가 정상적으로 동작하였다면 아래와 같이 리포트가 생성된다.

html report : codeCoverage/<moduleName>/index.html 파일 실행
xml report : codeCoverage/<moduleName>.xml 파일 실행

 

이 글이 많은 안드로이드 개발자들에게 도움이 되었으면 한다.

 

읽어주셔서 감사합니다.

 

출처

https://medium.com/@korwin22/jacoco-for-android-e56bffedef48

https://mparchive.tistory.com/183

http://vgaidarji.me/blog/2017/12/20/how-to-configure-jacoco-for-kotlin-and-java-project/

https://medium.com/@andrey.fomenkov/kotlin-jacoco-tuning-compiler-to-skip-generated-code-935fcaeaa391

댓글