본문 바로가기

Android/Utility

[Android] Jacoco를 사용하여 코드 커버리지 확인하기.

728x90

이전에 한창 테스트 코드에 관련하여 기본 개념을 공부할 때 코드 커버리지를 확인하는 방법이 있다는 것을 확인하고,

나중에 시간 되면 확인해봐야겠다 싶어서 메모해두었는데..

완전히 잊고 있다가 발견하게 되어 이에 대하여 적용하고 글을 작성해보려 한다.


우선,

코드 커버리지란 무엇인가?

 

테스트 케이스가 얼마나 충분한가를 나타내는 지표로,
테스트를 진행하였을 때 코드 자체가 얼마나 실행되었는지에 대한 수치.

라고 한다.

 

즉, 개발자가 작성한 테스트 코드를 실행시켜 보고, 해당 테스트 코드가 작성된 코드를 얼마만큼 검증했는지를 수치로 나타낸 것이다.

이에 대해서는 이후 결과를 보면 확실하게 이해가 가능하니, 이해가 잘 되지 않아도 상관없다.

 

여기서 테스트는 크게 블랙박스 테스트와 화이트박스 테스트로 나누어지는데,

이에 대해 간단하게 설명하자면,

 

  • 블랙박스 테스트 : 객체 내부를 알 필요 없이 입력에 따라 원하는 결과 값이 나오는지 확인하는 테스트로, 사용자 관점의 테스트이다.
  • 화이트박스 테스트 : 객체 내부를 확인하고 검증하는 테스트로, 개발자 관점으로 진행되는 테스트이다. 코드 커버리지는 화이트박스 테스트의 일부이다.

라고 간단하게 설명이 가능하다.

테스트 코드를 작성하고, 이에 따른 커버리지를 확인하는 것이기 때문에 화이트박스 테스트의 결과로 코드 커버리지를 알 수 있는 것이다.

 

코드 커버리지를 측정하는 기준으로는 구문, 조건, 결정으로 나뉘고 각 기준에 따라 커버리지가 나타내는 수치가 다른 의미를 갖게 된다.

 

  • 구문 (Statement) : 라인(Line) 커버리지라고도 하며, 코드 Line이 얼마만큼 실행되었는지 확인한다. 괄호와 같은 라인을 제외하고, 테스트 코드를 실행했을 때 실행된 라인의 %를 커버리지 값으로 나타낸다.
  • 조건 (Condition) : 사용되는 모든 조건식에서 true/false 값을 모두 충족하는지 확인한다.
// insert 1,0 / 0,1
fun sample(a: Int, b: Int) {
    // 1,0 = ture, false
    // 0,1 = false, true
    if (a > 0 && b > 0) { // 조건
        
    }
}

위와 같이 되어있을 때, TestCode가 sample 함수를 1,0 , 0,1 값을 넣어 호출했다면 조건을 모두 만족하게 되는 것이다.

여기서, true/false 값이라고 해서 if 조건문 전체의 결과 값이라고 생각할 수 있는데, 각각의 조건인 a > 0 , b > 0 의 조건의 true, false 값을 확인해야 한다.

  • 결정 (Decision) : 브랜치(Branch) 커버리지라고도 하며, 모든 조건식이 true/false를 모두 충족하는지 확인한다.
    위의 예제와 동일한 조건이라고 하면, 1,0 / 0,1의 경우 충족하지 못하고, 1,1 / 0,0의 경우 충족하게 된다.

이런 조건에서, Jacoco라는 라이브러리는 구문, 결정 커버리지를 사용하여 커버리지의 결과를 보여주게 된다.


Jacoco 라이브러리를 사용하기 앞서,

Jacoco란 무엇인가?

 

Java 코드의 커버리지를 체크하는 라이브러리.
Java 코드라고 하지만, 0.8.5 버전 이상에서는 큰 설정 없이 Kotlin도 함께 체크가 가능하다.

 

위에 설명한 코드 커버리지를 쉽게 체크하기 위한 라이브러리라고 생각하면 된다.

기존에는 Kotlin과 함께 체크하기 위해서는 추가적인 설정을 해주어야 했다고 하지만, 0.8.5 버전부터는 별다른 설정 없이 Kotlin도 함께 커버리지 체크가 가능하다고 한다.

 

코드 커버리지를 확인하기 위해서는 테스트 코드가 필요하기 때문에, 이전에 작성해둔 유닛 테스트 샘플 프로젝트를 사용하여 Jacoco를 적용시켜 보았다.

 

처음으로,

Project 범위의 Gradle에서 Jacoco를 추가해 주도록 한다.

 

buildscript {
    ext.kotlin_version = "1.5.0"
    ext.jacocoVersion = "0.8.5"
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.2.1"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jacoco:org.jacoco.core:$jacocoVersion"
    }
}

 

다음으로 Module 범위의 Gradle에서 플러그인과 필요한 코드를 작성해주도록 한다.

 

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'jacoco'
}

 

android {
    ...

    buildTypes {
        debug {
            testCoverageEnabled true
        }

        ...
    }
   ...
    testOptions {
        unitTests.includeAndroidResources = true
        unitTests.returnDefaultValues = true

        unitTests.all {
            jacoco {
                includeNoLocationClasses = true
            }
        }
    }
}

 

관련이 없는 부분은...으로 생략하였다.

 

debug일 경우에만 코드 커버리지를 사용하기 위하여 debug 블록 안에 testCoverageEnabled 값을 true로 설정해 주었고, 테스트 옵션에서 includeNoLocationClasses 값을 true로 설정함으로써 Test 타입을 가진 모든 파일을 찾을 수 있도록 설정해 주었다.

 

// dependsOn : 유닛 테스트를 수행하는 태스크 이름으로 설정.
task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) {

    // reports 생성 확장자
    reports {
        xml.enabled = true
        html.enabled = true
    }

    def mainSrc = "${project.projectDir}/src/main/java"
    sourceDirectories.setFrom(files([mainSrc])) // 커버리지를 측정할 소스 디렉터리

    // 커버리지에서 제외할 파일
    def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*']
    def debugTree = fileTree(dir: "${buildDir}/intermediates/classes/debug", excludes: fileFilter)
    classDirectories.setFrom(files([debugTree])) // 컴파일 결과 파일이 있는 디렉터리 지정.

    // 커버리지 측정 결과를 저장할 파일
    executionData.setFrom(fileTree(dir: "${buildDir}/jacoco/testDebugUnitTest.exec"))
}

 

 

 

주석으로 해당 코드들이 어떤 것을 의미하는지 작성해 두었으며, 추가적인 코드 없이 기본적으로 코드 커버리지를 사용하기 위하여 필요한 부분만 선언해두었다.

 

이곳에서 선언한 값들은 별도의 커스텀을 하지 않았다면 그대로 사용해도 상관없으며, fileFilter 부분만 DI를 사용하는 경우 추가해야 하는 항목들이 존재한다.

필자는 별도로 DI도 사용하지 않고 커스텀하여 사용하는 부분이 없었기 때문에 기본 값 그대로 사용하였다.

 

이렇게 선언을 하였으면, gradle Sync를 맞춘 후에 하단의 Terminal을 사용하여 report를 생성해보도록 한다.

 

gradlew connectedCheck
gradlew testDebugUnitTest

 

 

각각 라인을 호출하게 되면, 계측 테스트와 로컬 Unit 테스트를 수행하게 된다.

 

이때, 다음과 같은 에러가 발생하는 경우가 있는데

 

java.lang.NoClassDefFoundError: jdk/internal/reflect/GeneratedSerializationConstructorAccessor1

 

해당 에러가 발생하는 정확한 이유는 확인하지 못하였지만, 아래와 같이 코드를 한 줄 추가하면 정상적으로 해결이 가능하다.

 

unitTests.all {
    jacoco {
        // Test 타입의 파일을 모두 확인하기 위하여 설정.
        includeNoLocationClasses = true
        // java.lang.NoClassDefFoundError: jdk/internal/reflect/GeneratedSerializationConstructorAccessor1
        // 해당 오류가 발생할 경우 추가.
        excludes = ['jdk.internal.*']
    }
}

 

이처럼 선언하고 다시 커버리지를 수행하기 위한 커맨드를 입력하면, 정상적으로 성공하는 것을 확인할 수 있을 것이다.


그렇다면, 코드 커버리지에 대한 리포트는 어디에 저장이 될까?

 

executionData.setFrom(fileTree(dir: "${buildDir}/jacoco/testDebugUnitTest.exec"))

 

Gradle에 선언한 위치인데, 해당 위치에는 exec 확장자의 파일이 들어가 있기 때문에 간단하게 파일을 열어서 확인할 수 없다.

 

 

위의 루트에서, jacoco 파일이 아닌 reports 파일에 들어가 보자.

 

 

필자는 다음과 같은 파일을 확인할 수 있었는데, 위에서부터 계측 테스트 결과, 코드 커버리지, 로컬 테스트 결과 파일이 들어가 있다.

각 폴더를 들어가 보면, index.html 파일을 확인할 수 있는데 그것을 열어보도록 하자.

 

 

우선, 계측 테스트에 대한 결과를 확인할 수 있었다.

 

Packages를 클릭하면 classes로 이동하며, class에서는 계측 테스트가 실행된 클래스를 확인할 수 있다.

계측 테스트를 수행한 파일을 클릭하게 되면

 

 

이와 같이 수행된 테스트 함수의 이름과, 실행 시간을 알 수 있다.

 

다음으로는, 유닛 테스트에 대한 결과를 확인해보자.

 

 

필자는 유닛 테스트에는 일부러 실패하는 케이스를 넣어두었다.

이처럼 실패한 테스트가 메인으로 나오고, packages, classes는 계측 테스트와 동일하게 테스트가 수행된 패키지와 클래스가 나오게 된다.

 

 

해당 환경에서는 로컬 테스트 클래스가 2개이기 때문에 각 클래스 별로 테스트 개수, 실패 개수를 확인할 수 있다.

 

 

클래스를 선택하여 들어가면 이처럼 어떻게 오류가 발생하였는지, 실패한 테스트 함수는 무엇인지에 대한 정보도 확인이 가능하다.

 

마지막으로, 코드 커버리지에 대한 결과를 확인해보자.

 

 

이처럼 결과가 나오게 된다.

Instructions의 경우 Jacoco가 계산하는 최소 단위이며, Branches는 결정 커버리지라고 생각하면 된다.

Instructions가 구문 커버리지와 일치하는 것인가 싶었는데 꼭 그렇지만은 않은 것으로 보인다.

Branches에서 34%만큼 확인되지 않은 케이스가 있기 때문에 이것들을 찾아서 테스트 코드를 작성하게 되면 모든 조건에 따른 테스트 코드를 작성하게 되는 것이다.

 

*

22.07.09 추가.

Multi Module 구조에서 사용할 경우, 커버리지 확인이 필요한 Module에 Gradle 설정을 추가해주고, 터미널로 실행해주면 된다.

 

터미널로 실행 시, 위에 작성한 코드 앞에 모듈을 명시해주고 수행해주어야 정상적으로 동작한다.

 

gradlew :presentation:connectedCheck
gradlew :ModuleName:testDebugUnitTest

 

이처럼 실행주면 해당 모듈의 Build > reports 폴더 내부에 테스트 결과와 커버리지 결과가 저장된다.

 

또한, connectedCheck를 통해 계측 테스트를 진행하게 되는데, 계측 테스트 시에 에러가 발생하는 경우, 정상적으로 coverage 파일이 생성되지 않으니 이점을 인지하고 테스트를 진행해야 한다.

 

CleanArchtecture 관련 Sample코드에 Jacoco 작업을 진행하였으니, 멀티 모듈의 경우 해당 코드를 확인하길 바란다.


필자가 Jacoco를 적용한 프로젝트는 테스트 코드를 사용해보기 위한 아주 기본적인 화면만 그려놓고, 테스트를 진행한 경우이기 때문에 별 다른 테스트 코드를 작성하지 않았지만 상당히 높게 나오게 되었다.

하지만, 실 프로젝트에서 테스트 코드를 작성하고 별도의 옵션 같은 것들을 추가로 작성한 후 jacoco를 사용하게 되면 굉장히 유용하게 사용이 가능할 것으로 보인다.

 

아직 실무에서 테스트 코드를 작성해본 적은 없지만, 작성하게 되다면 Jacoco에 대하여 기본적인 것이 아닌 조금 더 확장할 수 있도록 공부한 후에 적용시켜 봐야겠다는 생각을 하게 되었다.

우아한형제들 기술 블로그에서 작성된 Jacoco에 대한 글을 확인해보니, 기본적인 세팅으론 턱없이 부족한 것으로 보이기 때문에 추후에 조금 더 확장해볼 기회가 있으면 적용해보고 글을 추가로 작성할 예정이다.

 

해당 게시글에 사용된 예제 코드는 Github에 업로드해두었다.

https://github.com/HeeGyeong/UnitTestSample

 

GitHub - HeeGyeong/UnitTestSample

Contribute to HeeGyeong/UnitTestSample development by creating an account on GitHub.

github.com

 

728x90