이번에 다룰 주제는 테스트 코드 커버리지 분석 도구 Jacoco이다.
테스트 코드를 작성하며 내가 테스트 코드를 잘 작성하고있는지에 대한 의구심이 생겼다. 또한 테스트 케이스를 얼마나 작성해야하는지 궁금하게 되었다. 해당 문제를 해결하기 위해서 검색을 하던중 Jacoco라는 도구를 발견하였다. 해당 도구를 현재 진행중인 프로젝트에 적용하여 작성한 테스트 코드들이 얼마나 잘 작성되었는지 확인해보기로 하였다.
코드 커버리지는 개발된 소프트웨어의 테스트 케이스가 소스 코드의 어느 정도를 실행하였는지를 수치로 나타내는 지표이다. 이 지표를 통해 개발자는 테스트가 충분히 이루어졌는지, 어떤 부분이 누락되었는지를 파악할 수 있다. 일반적으로 코드 커버리지는 구문(Statement), 조건(Condition), 결정(Decision) 커버리지로 나뉘며, 이는 테스트가 코드의 어떤 구조를 얼마나 커버하는지에 따라 구분된다.
JaCoCo(Java Code Coverage)는 Java 프로그램을 위한 코드 커버리지 분석 도구이다. 이 도구는 개발 과정에서 테스트 케이스가 소스 코드의 어느 부분을 실행했는지 시각적으로 보여주며, 개발자가 코드의 테스트 커버리지를 쉽게 파악하고 향상시킬 수 있도록 지원한다.
JaCoCo를 사용함으로써 개발자는 테스트의 빈틈을 쉽게 발견하고, 코드의 품질을 지속적으로 개선할 수 있다. 또한, 팀 내에서 코드 커버리지 목표를 설정하고 이를 달성하기 위한 노력을 강화할 수 있는 기회를 제공한다.
Jacoco는 프로젝트에 간단하게 적용할 수 있다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.1'
id 'io.spring.dependency-management' version '1.1.4'
id 'jacoco' // jacoco
}
test {
useJUnitPlatform()
// finalizedBy jacocoTestReport // Generates report after tests are run
}
// JaCoCo configuration
jacoco {
toolVersion = "0.8.10"
reportsDirectory = layout.buildDirectory.dir('jacocoReport')
}
jacocoTestReport {
dependsOn test
reports {
xml.required = true
csv.required = false
html.required = true
}
def Qdomains = []
for (qPattern in '**/QA'..'**/QZ') { // qPattern = '**/QA', '**/QB', ... '*.QZ'
Qdomains.add(qPattern + '*')
}
afterEvaluate {
classDirectories.setFrom(files(classDirectories.files.collect {
fileTree(dir: it, exclude: [
'**/dto/**',
'**/event/**',
'**/*InitData*',
'**/*Application*',
'**/exception/**',
'**/service/alarm/**',
'**/aop/**',
'**/config/**',
'**/MemberRole*'
] + Qdomains)
}))
}
finalizedBy 'jacocoTestCoverageVerification'
}
jacocoTestCoverageVerification {
def Qdomains = []
for (qPattern in '*.QA'..'*.QZ') { // qPattern = '*.QA', '*.QB', ... '*.QZ'
Qdomains.add(qPattern + '*')
}
violationRules {
rule {
enabled = true;
element = 'CLASS'
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.80
}
limit {
counter = 'BRANCH'
value = 'COVEREDRATIO'
minimum = 0.80
}
excludes = [
'**.dto.**',
'**.event.**',
'**.*InitData*',
'**.*Application*',
'**.exception.**',
'**.service.alarm.**',
'**.aop.**',
'**.config.**',
'**.MemberRole*'
] + Qdomains
}
}
}
프로젝트에 적용한 build.gradle중 일부이다. 아래에서 자세하게 설명하겠다.
id 'jacoco' // jacoco
jacoco 플러그인을 추가하였다.
test {
useJUnitPlatform()
// finalizedBy jacocoTestReport // Generates report after tests are run
}
finalizedBy jacocoTestReport는 test를 실행 후, jacocoTestReport를 실행하도록 할 수있다. 나는 Jacoco 도입 이후에 깃허브 액션을 통한 CI를 구축할때 Test과정과 jacocoTestReport 과정이 병렬적으로 진행되도록 할 계획이기 때문에 제외하였다.
jacoco {
toolVersion = "0.8.10"
reportsDirectory = layout.buildDirectory.dir('jacocoReport')
}
위와 같이 분석 리포트가 생성되는 경로를 지정할 수 있다.
build/jacocoReport 라는 디렉토리가 생성되고 해당 디렉토리 안에 리포트가 생성된다.
jacocoTestReport {
dependsOn test
reports {
xml.required = true
csv.required = false
html.required = true
}
def Qdomains = []
for (qPattern in '**/QA'..'**/QZ') { // qPattern = '**/QA', '**/QB', ... '*.QZ'
Qdomains.add(qPattern + '*')
}
afterEvaluate {
classDirectories.setFrom(files(classDirectories.files.collect {
fileTree(dir: it, exclude: [
'**/dto/**',
'**/event/**',
'**/*InitData*',
'**/*Application*',
'**/exception/**',
'**/service/alarm/**',
'**/aop/**',
'**/config/**',
'**/MemberRole*'
] + Qdomains)
}))
}
finalizedBy 'jacocoTestCoverageVerification'
}
- dependsOn test 로 test task 실행 뒤 실행되도록 할 수 있다.
- reports 설정을 통해 자신이 원하는 파일 형식으로 저장할 수 있다.
- CI구축을 위해서는 xml파일과 html이 필요하다.- afterEvaluate{ ... } 구문의 excludes : [ ... ] 안에 분석 리포트에서 제외할 클래스를 선택할 수 있다.
- QueryDSL을 사용하면 QDomain이라는 것이 존재하게 된다. 해당 클래스를 제외하기 위해 "def Qdomains"QDomain을 담아 제외하도록 하였다.- finalizedBy 'jacocoTestCoverageVerification' 를 통해 jacocoTestReport 이후에 jacocoTestCoverageVerification가 실행되도록 하였다.
jacocoTestCoverageVerification {
def Qdomains = []
for (qPattern in '*.QA'..'*.QZ') { // qPattern = '*.QA', '*.QB', ... '*.QZ'
Qdomains.add(qPattern + '*')
}
violationRules {
rule {
enabled = true;
element = 'CLASS'
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.80
}
limit {
counter = 'BRANCH'
value = 'COVEREDRATIO'
minimum = 0.80
}
excludes = [
'**.dto.**',
'**.event.**',
'**.*InitData*',
'**.*Application*',
'**.exception.**',
'**.service.alarm.**',
'**.aop.**',
'**.config.**',
'**.MemberRole*'
] + Qdomains
}
}
}
해당 설정은 커버리지 기준을 설정할 수 있다. 설정할 수 있는 커버리지 기준예시는 아래와 같다.
- 라인 커버리지를 최소한 80% 만족시켜야 함
limit { counter = 'LINE' value = 'COVEREDRATIO' minimum = 0.80 }
- 브랜치 커버리지를 최소한 80% 만족시켜야 함
limit { counter = 'BRANCH' value = 'COVEREDRATIO' minimum = 0.80 }
또한, jacocoTestReport와 다른 방식으로 커버리지 검증에서 제외할 경로도 추가할 수 있다.
커버리지 기준을 만족하지 못할경우, 사진과 같이 오류가 발생한다.
위에 과정까지만 수행할 경우, lombok을 통해 생성한 @builder와 @entitiy등이 coverage에 포함대상되어 정확한 코드 커버리지 측정이 불가능하다.
lombok.addLombokGeneratedAnnotation = true
lombok.addLombokGeneratedAnnotation = true를 통해서 lombok을 통해 생성한 어노테이션들을 검증 대상에서 제외할 수 있다.
Jacoco Report를 확인하기 위해서는 아래 명령어들을 입력해 주면 된다.
./gradlew test
test를 실행하는 코드이다. 해당 명령어를 통해서 Jacoco Report를 확인하기 위해서는 "finalizedBy jacocoTestReport" 설정을 해주어야만 한다.
./gradlew jacocoTestReport
jacocoTestReport를 생성하는 코드이다. jacocoTestReport 설정에서 설정해주었던 "dependsOn test"을 통해서 test를 먼저 실행시켜주고 finalizedBy 'jacocoTestCoverageVerification' 를 통해 jacocoTestReport 이후에 jacocoTestCoverageVerification가 실행되어 커버리지를 검증한다.
또는 intellij의 기능을 사용할 수 있다.
우측 Gradle에서 Verification의 test 또는 JacocoTestReport를 통해 실행 가능하다.
이전에 분석 결과 경로 설정헀던 경로로 Jacoco Report가 생성된다.
Jacoco Report를 html로 생성하였을 경우, 직접 확인이 가능하다.
라인 커버리지와 브랜치 커버리지를 눈으로 확인할 수 있다.
상세 화면에 들어갈 경우, 사진과 같이 같이 라인 커버리지를 확인할 수 있다.
Jacoco를 통해 코드 커버리지를 측정해보았는데, 라인 커버리지와 브랜치 커버리지를 둘다 적용하였음에도 큰 문제없이 프로젝트에 적용할 수 있었던 것을 보아서는 평소에 테스트 코드를 잘 작성하고 있었던 것 같다. 앞으로는 프로젝트를 시작할때부터 Jacoco를 적용하여 계속해서 코드 커버리지를 측정하며 진행할 생각이다. 코드 커버리지가 80%가 넘도록 하는것이 좋은 코드의 기준이라는 것을 명심하고 앞으로도 열심히 테스트 코드를 작성할 생각이다.