JaCoCo 적용기
Intro
현재 리팩토링을 진행하며 test 코드를 열심히 짜고 있는 중이다. 그런데 현재 테스트코드를 잘 짜고 있는지가 궁금해졌다. 그래서 코드 커버리지 측정도구인 Jacoco를 이용해 현재 나의 test 코드를 평가해보려 한다.
오늘은 Jacoco가 무엇인지, 동작 방식, 적용 방법에 대해서 설명하겠다.
JaCoCo란?
JaCoCo (Java Code Coverage)는 자바 프로그램의 코드 커버리지를 측정하기 위한 도구이다. 코드 커버리지는 테스트 코드가 애플리케이션 코드의 몇 퍼센트를 실행했는지 나타내는 지표로, 소프트웨어 테스트의 품질을 평가하는 데 중요한 역할을 한다.
주요 특징
- 라인 커버리지(Line Coverage): 각 코드 라인이 실행된 횟수를 측정
- 분기 커버리지(Branch Coverage): 조건문에서 각 분기(예: if-else)의 실행 여부를 측정
- 메서드 커버리지(Method Coverage): 각 메서드의 호출 여부를 측정
- 클래스 커버리지(Class Coverage): 각 클래스의 커버리지 상태를 측정
주요 기능
- 리포트 생성: JaCoCo는 HTML, CSV, XML 등 다양한 형식의 보고서를 생성하여 커버리지 결과를 시각화한다.
- IDE 통합: IDE와 통합되어 개발자가 코드 커버리지를 실시간으로 확인할 수 있다.
- Maven 및 Gradle 플러그인: Maven 및 Gradle 빌드 도구와 통합되어 프로젝트 빌드 시 자동으로 코드 커버리지 보고서를 생성한다.
- 인터셉터: 바이트코드 수준에서 인터셉트하여 코드 커버리지를 측정하므로, 소스 코드를 변경할 필요가 없다.
장점
- 테스트 효율성 향상: 코드의 어떤 부분이 테스트되지 않았는지 쉽게 확인할 수 있어, 테스트의 품질을 향상시킬 수 있다. 실제로 리포트를 보면 굉장히 자세히 설명되어 있어서 어떤 테스트코드가 부족한지 한눈에 알 수 있다.
- 자동화된 빌드 시스템과 통합: Maven, Gradle 등과 통합되어 CI/CD 파이프라인에서 손쉽게 사용 가능하다.
- 광범위한 통합: 다양한 IDE 및 도구와 통합되어 편리하게 사용할 수 있다.
JaCoCo 원리
JaCoCo 동작 방식
기본적으로 gradle의 test 태스크(task)가 실행되면, JaCoCo Java Agent가 테스트 프레임워크가 로드하는 모든 클래스에 적용된다. 테스트가 실행될 때, JaCoCo는 코드 커버리지 데이터를 수집하고 `. exec` 파일로 저장한다.
Gradle이 기본적으로 제공하는 Task 간의 의존 관계 설정 방식
기본적으로 gradle은 Task 간의 의존 관계 설정 방식을 제공한다. 또한 Gradle에서 Task 간의 의존 관계는 전이적(transitive)으로 적용된다.
- build 태스크는 assemble 태스크에 의존
- assemble 태스크는 check 태스크에 의존
- check 태스크는 test 태스크에 의존
따라서 test 태스크가 실행되고 성공적으로 완료되면, Gradle은 이를 기반으로 check, assemble, build 태스크가 순차적으로 실행된다. 이는 Gradle의 기본 동작 방식에 따라 자동으로 이루어진다.
jacocoTestReport 태스크 동작 방식
jacocoTestReport 태스크는 `. exec' 파일을 분석하여 다양한 형식의 커버리지 리포트를 생성한다. jacocoTestReport는 check 태스크의 일종이므로 해당 테스트가 실행되면 jacocoTestReport → assemble → build 태스크 순으로 실행된다.
jacocoTestCoverageVerification 태스크 동작 방식
jacocoTestCoverageVerification 태스크는 설정한 코드 커버리지 기준을 검증하는 역할을 수행한다. 해당 태스크 또한 check 태스크의 일종이므로 해당 테스트가 실행되면 jacocoTestCoverageVerification → assemble → build 태스크 순으로 실행된다. 따라서 검증에 실패하면 빌드에 실패하게 된다.
JaCoCo 적용 방법 : build.gradle 설정
플러그인 설정
plugins {
id 'jacoco'
}
JaCoCo는 plugin 블록을 통해 추가되어 Gradle 환경을 확장한다.
버전 설정
jacoco {
toolVersion = '0.8.10'
}
JaCoCo의 버전을 설정한다.
jacocoTestReport 태스크 설정
jacocoTestReport {
reports {
html.required = true
xml.required = false
csv.required = false
}
afterEvaluate {
classDirectories.setFrom(files(classDirectories.files.collect{
fileTree(dir: it, include: [
'**/service/**',
'**/interceptor/api/**',
'**/interceptor/view/**'
])
}))
}
finalizedBy 'jacocoTestCoverageVerification'
}
jacocoTestReport
jacoco가 수집한 커버리지 데이터를 통해 리포트를 생성하는 태스크이다.
reports
생성될 리포트의 종류를 설정할 수 있다. 나는 html 형식의 리포트만 생성하기 위해 나머지 형식들의 보고서 생성은 false로 설정하였다.
afterEvaluate
빌드 스크립트의 모든 설정이 완료된 후에 실행되는 블록이다. 위의 코드에서는 jacocoTestReport 태스크의 설정이 완료된 후에 실행된다.
classDirectories.setFrom(...)
해당 설정은 jacocoTestReport 태스크를 통해 리포트를 만들 때 포함할 클래스들을 필터링하는 코드이다. 설정하지 않을 시(기본값)는 전체 파일을 기준으로 한다. 나의 경우 현재는 서비스와 인터셉터 클래스만 테스트 코드가 존재해서 위처럼 설정하였다.
jacocoTestReport의 include 또는 exclude에서 파일 경로를 표현할 때는 Ant 스타일의 디렉토리 구조를 기반으로 한 와일드카드 방식으로 작성해야 한다.
finalizedBy
jacocoTestReport 태스크가 완료된 후 jacocoTestCoverageVerification 태스크가 실행되도록 설정한다.
jacocoTestCoverageVerification 태스크 설정
jacocoTestCoverageVerification {
violationRules {
// 프로젝트 전반의 기본 커버리지 목표
rule {
element = 'CLASS'
includes = [
'*.service.*',
'*.interceptor.api.*',
'*.interceptor.view.*'
]
limit {
counter = 'BRANCH'
value = 'COVEREDRATIO'
minimum = 0.80 // 클래스 단위 분기 커버리지 기준 (80%)
}
}
}
}
jacocoTestCoverageVerification
jacoco가 수집한 커버리지 데이터를 기준으로 특정 커버리지 목표가 달성되었는지 확인하는 태스크이다.
violationRules
코드 커버리지 검증 규칙을 설정하는 블록이다.
rule
적용될 규칙, 적용 범위, 적용 단위 등을 설정한다.
element
어떤 단위의 코드 커버리지를 검증할 것인지를 설정한다.
- BUNDLE: 프로젝트 전체 또는 모듈과 같은 큰 단위
- PACKAGE: 특정 패키지 단위
- CLASS: 개별 클래스 단위
- METHOD: 개별 메서드 단위
includes
검증할 클래스들의 패턴을 지정한다.
limit
커버리지 검증 규칙을 설정한다.
violationRules의 includes 또는 excludes에서 파일 경로를 표현할 때는 패키지 이름을 기반으로 한 패턴 매칭방식으로 작성해야 한다.
counter
커버리지를 측정하는 기준을 정의한다.
- INSTRUCTION: 개별 JVM 바이트코드 명령어 단위로 측정.
- BRANCH: 조건문(분기) 단위로 측정
- LINE: 소스 코드의 각 라인 단위로 측정
- METHOD: 메서드 단위로 측정
- CLASS: 클래스 단위로 측정
value
커버리지의 종류로 선택된 커버리지를 사용하여, 커버된 비율을 기준으로 설정한다.
- TOTALCOUNT: 해당 커버리지 유형의 전체 항목 수를 측정
- COVEREDCOUNT: 커버된 항목의 수를 측정
- MISSEDCOUNT: 커버되지 않은 항목의 수를 측정
- COVEREDRATIO: 커버된 항목의 비율을 측정 (커버된 항목 수 / 전체 항목 수)
- MISSEDRATIO: 커버되지 않은 항목의 비율을 측정 (커버되지 않은 항목 수 / 전체 항목 수)
test 태스크 설정
test {
useJUnitPlatform()
finalizedBy 'jacocoTestReport'
}
finalizedBy
test 태스크를 실행할 경우 jacocoTestReport 태스크가 자동 실행될 수 있도록 finalizedBy 블록을 이용하여 설정하였다. 만약 test 태스크 실행 시 자동 리포트 생성을 하지 않고 싶다면 위의 코드는 생략하면 된다.
주의!
jacocoTestReport 태스크와 jacocoTestCoverageVerification 태스크의 파일 경로 표현 방식이 다르다.
jacocoTestReport 태스크는 Ant 스타일의 디렉토리 구조를 기반으로 한 와일드카드 방식 (**/**)이고,
jacocoTestCoverageVerification 태스크는 패키지 이름을 기반으로 한 패턴 매칭방식 (*.*)이다.
실습
JaCoCo를 적용하고 리포트를 생성해 보았다. 먼저 커버리지 기준을 클래스(CLASS) 단위의 결정(BRANCH) 커버리지, 구문(LINE) 커버리지로 설정하였다. 커버리지 기준은 모두 80%로 잡았다.
기대되는 마음으로 test 태스크를 실행하여 리포트를 생성하였다.
먼저 콘솔 창에는 빌드실패로 떴다. 흠... 로그를 확인해 보니 아래와 같았다.
두 개의 파일이 기준을 넘지 못했다. ApiAuthInterceptor 파일의 경우 결정 커버리지 기준을 넘지 못했고(75%), ViewAuthNIngerceptor의 경우 구문 커버리지 기준을 넘지 못했다 (77%).
리포트를 확인해 보니 아래와 같았다.
리포트에 들어가서 파일들을 클릭해 보니 어떤 부분의 테스트가 부족한지 상세하게 알 수 있었다.
해당 부분을 기준으로 아래와 같은 테스트를 생성하였다.
이후 test 태스크를 실행하였다.
빌드에 성공하여 리포트를 확인해 보았다.
부족했던 테스트를 추가하니 커버리지가 100%가 되었다.
하지만 계속 눈에 밟히는 게 있었으니...
설정한 커버리지(80%) 기준을 넘었기 때문에 그냥 둬도 되지만 자꾸 눈에 밟혀서 어쩔 수 없이(?) 코드를 짜기 시작했다.
이로써 모든 커버리지를 100% 달성하였다!!
후기
JaCoCo가 굉장히 사용하기가 편하고 직관적이라 적용하는 데에 어려움은 없었으나...
jacocoTestReport 태스크는 Ant 스타일의 디렉토리 구조를 기반으로 한 와일드카드 방식 (**/**)이고,
jacocoTestCoverageVerification 태스크는 패키지 이름을 기반으로 한 패턴 매칭방식 (*.*)이다.
이 부분으로 인해 2시간 정도 날려먹었다.... 공식 문서에도 자세히 설명되어있지 않아서 뭐가 문제였는지 정말 파악하기가 어려웠다.
어찌 됐든 저 부분 말고는 크게 열받는(?) 부분은 없었다.
그리고 나는 항상 게임을 하면 부 퀘스트 깨는 맛에 하는데, JaCoCo를 적용해 놓으니 마치 부퀘스트를 깨는 느낌이다. 무조건 커버리지 100%를 달성해야 할 것 같은 느낌...
이유가 좀 이상한 것 같지만(ㅎㅎ) JaCoCo로 인해서 test 코드 짜는 게 좀 더 즐거워졌고, 내가 test 코드를 잘 짜고 있나 싶은 막연함이 있었는데 그 부분이 해소되었다.
출처
https://www.jacoco.org/jacoco/trunk/doc/
https://docs.gradle.org/current/userguide/jacoco_plugin.html#jacoco_plugin