본문 바로가기
언어 공부/Java

[Java] Jacoco 잘 사용하기 (feat. 심화)

by 희조당 2023. 10. 26.

🙋 들어가며

애플리케이션의 안정성을 나타내는 지표는 무엇일까요? 바로 테스트 커버리지입니다.

테스트 커버리지란 우리 시스템에 얼마나 테스트 코드가 작성되었는지 나타내는 지표입니다.

자바 진영에는 Jacoco라는 도구가 존재하는데 간단하게 사용법을 알아보겠습니다. 😋


💄 Jacoco

Jacoco란, 자바 진영의 테스트 커버리지 측정 도구입니다.

테스트를 작성하면 조건에 따라 커버리지를 측정하고 거기에 따른 Report를 제공합니다.

이 글에서 사용한 환경은 다음과 같습니다.

Spring boot : 3.1.5
Java : 17
Jacoco : 0.8.8
Build Tool : Gradle

🚀 Gradle에 적용하기

Jacoco 플러그인을 Gradle에 다음과 같이 적용합니다.

plugins {
    ...
    id 'jacoco'
}

jacoco {
    toolVersion = "0.8.8"
    // reportsDir = file("$buildDir/customJacocoReportDir")
}

 

reportsDir를 통해서 경로를 지정할 수 있습니다.

Default 경로는 다음과 같습니다.

/build/reports/jacoco/test/jacocoTestReport.xml

📝 Report 생성하기

단순하게 플러그인만 적용한다고 커버리지가 측정되지 않습니다.

앞서 언급했던 것처럼 조건에 따라 커버리지를 측정하므로 관련된 설정을 추가해야 합니다.

jacoco에 사용되는 gradle task를 설명드리겠습니다.

1️⃣ jacocoTestReport

커버리지 결과를 리포트 형태로 만들어주는 작업입니다.

읽기 편한 html 파일로 만들 수 있고, SonarQube 등을 위한 xml, csv 파일로도 만들 수 있습니다.

 

dependsOn을 통해서 앞서 완료되어야 할 작업을 명시할 수 있습니다.

모든 테스트를 수행하고 실행해야 하므로 test를 명시해 주었습니다.

jacocoTestReport {
    dependsOn test

    reports {
        xml.required = true
        html.required = true
        csv.required = false
    }
}

2️⃣ jacocoTestCoverageVerification

테스트 커버리지 측정을 위한 조건을 명시하는 task입니다.

조건은 다양하게 적용시킬 수 있는데 violationRules 밑에 조건(rule)을 다음과 같이 명시하면 됩니다.

jacocoTestCoverageVerification {
    violationRules {
        rule {
            enabled = true
            element = 'CLASS'

            limit {
                counter = 'LINE'
                value = 'COVEREDRATIO'
                minimum = 0.00
            }

            limit {
                counter = 'METHOD'
                value = 'COVEREDRATIO'
                minimum = 0.00
            }
        }
    }
}

적용할 수 있는 조건들은 다음과 같습니다. (모든 설정을 명시하진 않겠습니다 😋)

  • enabled : 조건 적용 여부
  • element : 커버리지 체크 기준
    1. BUNDLE(default) : 패키지 번들
    2. PACKAGE
    3. CLASS
    4. SOURCEFILE
    5. METHOD
  • counter : 측정 대상
    1. LINE : 빈 줄을 제외한 실제 코드의 라인 수
    2. BRANCH : 조건문 등의 분기 수
    3. CLASS
    4. METHOD
    5. INSTRUCTION (default) : Java 바이트 코드 명령 수
    6. COMPLEXITY
  • value : 측정 기준
    1. TOTALCOUNT : 전체 개수
    2. MISSDCOUNT : 커버되지 않은 카운트
    3. COVEREDCOUNT : 커버된 개수
    4. MISSEDRATIO : 커버되지 않은 비율, 소수점으로 표현
    5. COVEREDRATIO (default) : 커버된 비율, , 소수점으로 표현
  • minimum : 최소 지표, 소수점으로 표현

📈 Task 적용하기

jacoco의 task들을 사용하려면 반드시 모든 테스트를 수행하는 test를 동작시킨 이후에 사용해야 합니다.

finalizedBy로 어떤 task가 완전히 수행되었다면 이어서 진행할 작업을 명시해 줄 수 있습니다.

test {
    useJUnitPlatform()
    finalizedBy jacocoTestReport
}

jacocoTestReport {
    ...
    finalizedBy 'jacocoTestCoverageVerification'
}

 

가장 손쉬운 방법이나 개인적으로 모든 작업을 표현하지 못한다고 생각합니다.

메서드(task)가 어떤 행위를 하는지 드러내고자 저는 다음과 같이 선언해서 사용합니다.

task testAndGenerateReport {
    group 'verification'
    description 'test w/ jacoco'

    dependsOn ':test', ':jacocoTestReport', ':jacocoTestCoverageVerification'

    tasks.jacocoTestReport.mustRunAfter tasks.test
    tasks.jacocoTestCoverageVerification.mustRunAfter tasks.jacocoTestReport
}

 

group으로 명시해 준다면 다음과 같이 gradle 명령에 추가된 것을 확인할 수 있습니다.

verification에 추가된 task

 

이후 —-dry-run 명령어로 실행하는 작업을 확인해 보면 다음과 같이 잘 동작하는 것을 볼 수 있습니다. 😋

실행되는 명령들

 

그리고 결과지가 잘 생성되었는지 실제로 돌려보고 앞서 언급한 경로로 가보면 다음과 같이 생성된 것을 확인할 수 있습니다.

🚨 측정 대상 제외시키기

위와 같이 설정하면 발생하는 문제가 있습니다.

바로 굳이 측정하지 않아도 괜찮은 대상 모두 측정한다는 점입니다. (ex. XXXApplication.class)

문제를 방지하기 위해 task에 조건을 추가해야 합니다.

1️⃣ 결과 리포트에 제외하기

jacocoTestReport에 다음과 같이 조건을 추가하면 테스트 리포트에 제외할 수 있습니다.

jacocoTestReport {
    ...

    // 결과 리포트에서 제외할 클래스들
    afterEvaluate {
        classDirectories.setFrom(files(classDirectories.files.collect {
            fileTree(dir: it, excludes: [
                            '**/XXXApplication.class'
                    ])
        }))
    }
}

 

특정 클래스를 위와 같이 선언해서 제외할 수 있습니다.

그리고 []인 점을 통해서 List도 적용시킬 수 있다는 점을 알 수 있습니다.

2️⃣ 커버리지 체크에서 제외하기

이번에는 jacocoTestCoverageVerification에 조건을 추가해서 제외하는 방법입니다.

jacocoTestCoverageVerification {
    violationRules {
        rule {
            ...

            // 제외할 클래스들
            excludes = [
                    '*.XXXApplication'
            ]
        }
    }
}

 

jacocoTestReport와 동일하게 제외할 수 있습니다.

반대로 측정할 클래스를 includes로 추가할 수도 있습니다.

🔥 테스트 대상 제외하기 (심화)

앞서 소개한 방법은 어떤 문제가 있을까요??

프로젝트의 규모가 점점 커지고 제외할 대상이 많아진다면 gradle script에 너무 많은 코드를 작성해야 합니다.

따라서, 한 곳에서 몰아두고 관리하면 보다 편할 것 같습니다.

프로젝트를 진행하면서 해결하기 위해 적용한 방법을 소개드리겠습니다. 😋

1️⃣ 한 파일에 몰기

가장 먼저 파일을 생성해서 제외할 대상을 모아서 관리합니다.

외부에서 읽을 일이 없다고 판단해서 어떠한 확장자도 선언하지 않고 다음과 같이 작성했습니다.

exclude라는 prefix를 붙이지 않아도 괜찮지만, 제외하는 대상임을 명시하기 위해서 사용했습니다.

// jacoco-exclude-class
exclude org/hejow/moon/Application
exclude org/hejow/moon/global/*
exclude org/hejow/moon/infra/*
...

2️⃣ 스크립트에 가져오기

앞서 작성한 파일을 불러와서 사용해야 합니다.

따라서 gradle script에 다음과 같은 코드를 추가해 줍니다.

def excludeClassesFromReport = new ArrayList<String>()
file('jacoco-exclude-classes').withInputStream {
    it ->
        excludeClassesFromReport.addAll(new BufferedReader(new InputStreamReader(it))
                .lines()
                .map(s -> s.substring(7).strip())
                .toList())
}

3️⃣ jacocoTestReport에 선언하기

스크립트에 가져온 파일을 다음과 같이 적용합니다.

jacocoTestReport {
    ...

    afterEvaluate {
        classDirectories.setFrom(files(classDirectories.files.collect {
            fileTree(dir: it, exclude: excludeClassesFromReport.stream()
                    .map(s -> s + ".class")
                    .toList())
        }))
    }
}

 

여기서 중요한 포인트가 있습니다. 눈썰미 좋으신 분들은 바로 예상하셨을거라고 생각합니다. 👍👍

앞선 예제에서 다음과 같이 제외할 대상을 명시했습니다.

 excludes: [
        '**/XXXApplication.class'
]

 

같은 형태로 적용하기 위해서 .class 확장자를 반드시 추가해야 하고 /를 통해서 계층 구조를 나타내야 합니다.

그렇기 때문에 stream으로 확장자를 붙이는 작업을 추가해 주었습니다.

4️⃣ jacocoTestCoverageVerification에 선언하기

마지막으로 다음과 같이 선언합니다.

jacocoTestCoverageVerification {
    violationRules {
        rule {
            ...

            excludes += excludeClassesFromReport.stream()
                    .map(s -> s.replace("/", "."))
                    .toList()
        }
    }
}

 

여기에서도 중요한 포인트가 있습니다.

바로 jacocoTestReport에서와 다르게 확장자를 필요로 하지 않고 / 대신에 .으로 계층 구조를 표현해야 합니다.

따라서 모든 조건을 만족시키기 위해서 파일을 다음과 같이 작성했던 것입니다.

// 확장자 ❌, '/'으로 계층 표현
exclude org/hejow/moon/Application
exclude org/hejow/moon/global/*
exclude org/hejow/moon/infra/*
...

👻 Lombok 제외하기

이제 마지막으로 하나만 남았습니다. 😜

우리의 편의를 돕는 롬복은 컴파일 시점에 추가적인 코드를 끼워넣습니다.

사용하는 시점에는 보이지 않지만 Jacoco의 스캔 대상에 포함된다는 것을 의미합니다. (getter, setter)

 

롬복이 생성하는 메서드를 제외하지 않으면 테스트 커버리지가 정확하지 않을 수 있습니다.

따라서, root에 다음과 같이 lombok.config를 추가해서 이 문제를 해결합니다.

lombok.addLombokGeneratedAnnotation=true

😋 정리

오늘 내용을 총 정리하면 다음과 같습니다.

  • Jacoco란, 자바 진영의 테스트 커버리지 측정 도구이다.
  • Jacoco를 사용하기 위해서는 plugin을 추가해야 한다.
  • 추가적인 Task를 작성해서 조건을 명시해야 한다.
  • jacocoTestReport는 테스트 결과를 리포트로 만들어준다.
  • jacocoTestCoverageVerification로 측정 조건을 명시한다.
  • 커버리지 측정 결과의 정확도를 위해서 클래스나 메서드를 제외하기도 해야 한다.

제가 고민한 방법을 공유했는데 더 좋은 방법이 있다면 꼭! 공유해주시기 바랍니다. 😋😋


Reference:

https://techblog.woowahan.com/2661/
https://docs.gradle.org/current/dsl/org.gradle.testing.jacoco.tasks.JacocoCoverageVerification.html
https://docs.gradle.org/current/userguide/jacoco_plugin.html

 

 

😋 지극히 개인적인 블로그지만 댓글, 조언 그리고 좋아요는 제 성장에 도움이 됩니다 😋

'언어 공부 > Java' 카테고리의 다른 글

[Java] Jasypt 알아보기  (0) 2023.10.21
[Java] Fixture Monkey 사용해보기  (2) 2023.10.17
[Java] JVM 알아보기  (0) 2023.08.25
[Java] 올바른 Collection 선택하기  (0) 2023.08.09
[Java] Hash란? (feat. Hash Collection)  (0) 2023.07.21

댓글