1. Jacoco란?
Java code coverage tool
Java Code의 coverage를 측정하는 라이브러리이다.
- 테스트를 실행하고, 그 결과를 html, csv, xml 파일을 통해 리포트 내려받기 가능
- 결과에 대한 기준치를 적용 가능
2. 그렇다면 code coverage란 무엇인가?
코드 커버리지의 개념 상위에 테스트 커버리지가 있다.
2-1. 테스트 커버리지(Test Coverage)
- 주어진 테스트 케이스에 의해 수행되는 소프트웨어의 테스트 범위를 측정하는 테스트 품질 측정 기준
- 기능 기반 커버리지, 라인 커버리지, 코드 커버리지의 3가지 유형이 있음
이 중에, 코드 커버리지는 소스 코드의 구문, 조건, 결정 등의 구조 코드 자체가 얼마나 테스트 되었는가를 측정하는 방법이다.
즉, 테스트 코드에서 내가 작성한 코드 자체가 얼마나 실행되었는가를 측정하는 방법이다.
코드 커버리지엔 또 여러가지 유형이 있지만, 대표적으로 구문 커버리지, 결정 커버리지, 조건 커버리지의 3가지 유형이 있다.
유형이 여러가지인 이유는 이 3가지 유형마다 측정기준이 다르다.
그 기준은 다음과 같다.
- 구문 커버리지는 프로그램 내 모든 코드를 적어도 한 번 이상 실행되면 충족되는 기준
- 결정 커버리지는 전체 조건식이 최소한 참/거짓 한 번을 가지게 되면 충족되는 기준
- 조건 커버리지는 결과와 상관없이 전체 조건식 결과와는 별개로 각 개별 조건식이 참/거짓 한 번을 가지게 되면 충족되는 기준
다시 돌아와서, Jacoco는 이런 코드 커버리지의 여러 유형들을 진행하고 분석해주는 도구이다.
이제 그럼 본격적으로 Jacoco를 적용해보겠다.
3. build.gradle 설정
3-1. 플러그인 추가
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.6'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
// jacoco
id 'jacoco' // 이 부분 추가
}
jacoco {
toolVersion = '0.8.8' // 이 부분 전체 추가
}
기존 gradle 설정에 Jacoco 플러그인을 추가한다. 그리고 버전을 적절하게 설정해준다. 나의 경우엔 다음과 같이 Dependencies에서 권장해준 버전을 설정해주었다.
툴 버전(toolVersion) 말고도 repostsDir = file()
을 통해 Jacoco 리포트 결과물 디렉토리를 설정할 수 있다. 하지만 ,아래의 jacocoTestReport
task에서 설정해 줄 수 있으므로 따로 적지 않는다.
플러그인을 추가하면 다음과 같이 jacocoTestReport
와 jacocoTestCoverageVerification
task가 gradle verification항목에 추가된다.
3-2. task 설정 - jacocoTestReport
플러그인을 추가하니, jacocoTestReport
와 jacocoTestCoverageVerification
task가 추가되었다.
간단히 살펴보자.
jacocoTestReport
는 코드 커버리지 진행 후 결과를 리포트로 저장해주는 작업이다.jacocoTestCoverageVerification
는 개발자가 build.gradle에 작성한 커버리지 기준을 만족하는지 검증해주는 작업이다.
✅ 여기서 제일 중요한 부분은 test와 위의 각각의 task들 간의 종속성 설정이다. 진행되는 순서가 중요하다는 것이다. 아무것도 모르는 상태에서 생각해봐도 test가 진행되고 나서 리포트를 생성해주고, 검증 작업이 이루어져야 할 것 같지 않은가?
실제로 그렇다!!! 그렇기 때문에, test -> jacocoTestReport -> jacocoTestCoverageVerification 순으로 실행되게끔 설정을 해주어야 한다.
다음과 같이 설정하면 된다. 일단 지금은 순서만을 보여주기 위해 간단한 설정만 들어가있다.
test {
useJUnitPlatform() // JUnit5를 사용하기 위한 설정
finalizedBy 'jacocoTestReport' // Test 이후 커버리지가 동작하도록 finalizedBy 추가
}
jacoco {
toolVersion = '0.8.8'
}
// 코드 커버리지 진행 후 결과를 리포트로 저장
jacocoTestReport {
dependsOn test
reports {
html.enabled true
xml.enabled true
csv.enabled true
// html 파일 위치 지정
html.destination file('build/reports/myReport.html')
}
finalizedBy 'jacocoTestCoverageVerification'
}
// 검증 단계
jacocoTestCoverageVerification {
violationRules {
rule {
...
}
}
}
test {}
영역에서 finalizedBy 'jacocoTestReport'
를 통해 test 진행 후 동작하도록 했다.
jacocoTestReport
에선 html, xml, csv 형식의 리포트를 모두 허용해주었고, 특히, html 파일 위치를 지정해주었다.
참고로, xml, csv로 저장하면 이를 소나큐브(SonarQube)에 전달해 결과에 따라 커밋성공/실패를 결정하는 식으로 사용될 수 있다고 한다.
jacocoTestReport
마지막에 finalizedBy 'jacocoTestCoverageVerification'
를 통해 순서를 보장해주었다.
3-3. task 설정 - jacocoTestCoverageVerification
검증 task이다.
직접 설정한 최소 수준을 달성하지 못하면 task는 실패를 하게 된다.
violationRules {}
에 여러 기준들을 직접 정할 수 있고, 이는 여러 rule
에서 정의한다.
// 검증 단계
jacocoTestCoverageVerification {
violationRules {
rule {
enabled = true // 활성화
element = 'CLASS' // 클래스 단위로 커버리지 체크
// includes = []
// 라인 커버리지 제한을 80%로 설정
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.80
}
// 브랜치 커버리지 제한을 80%로 설정
limit {
counter = 'BRANCH'
value = 'COVEREDRATIO'
minimum = 0.80
}
// 빈 줄을 제외한 코드의 라인수를 최대 200라인으로 제한
limit {
counter = 'LINE'
value = 'TOTALCOUNT'
maximum = 200
}
}
}
}
rule
의 설정값들을 살펴보자.
- enabled
- 테스트 검증을 위한 해당
rule
을 활성화할 것인지를 나타낸다. default는 true이다.
- 테스트 검증을 위한 해당
- element
- 테스트를 적용할 대상 혹은 테스트 측정 단위로 6개 유형이 존재한다.
- Default값은 BUNDLE
- BUNDLE : 패키지 번들(프로젝트 모든 파일을 합친 것)
- ✅CLASS : 클래스
- GROUP : 논리적 번들 그룹
- METHOD : 메서드
- PACKAGE : 패키지
- SOURCEFILE : 소스 파일
- limit
rule
의 상세 설정
- counter
- 커버리지 측정의 최소 단위로 마찬가지로 6개의 유형이 존재한다.
- Default는 INSTRUCTION : 가장 작은 측정 방식으로, Java 바이트코드를 읽는다.
- ✅BRANCH : 조건문(if, switch) 등의 분기 수에 대해 측정을 진행한다.
- ✅LINE : 라인이 한 번이라도 실행되면 실행된 것으로 간주, 빈 줄은 제외한다.
- CLASS : 클래스 내부의 메서드가 한 번이라도 실행된다면 실행된 것으로 간주한다.
- COMPLEXITY : 코드 복잡도
- METHOD : 메서드가 한 번이라도 실행되면 실행된 것으로 간주한다.
- value
- 커버리지 측정을 보여주는 방식을 정하는 부분이다.
- ✅Default는 COVEREDRATIO : 커버된 비율, 0부터 1사이의 숫자로 표현한다.
- COVEREDCOUNT : 커버된 개수
- MISSEDCOUNT : 커버되지 않은 개수
- MISSEDRATIO : 커버되지 않은 비율, 0부터 1사이의 숫자로 표현한다.
- TOTALCOUNT : 전체 개수
- minimum / maximum
jacocoTestCoverageVerification
의 성공 여부를 결정하는 기준이다.- 예를 들어, minimum 0.8을 입력하면 80% 이상을 통과해야 성공이다.
- maximum의 경우 코드의 라인 수를 제한할 때 사용한다.
- excludes
- 커버리지를 측정할 때 제외할 클래스를 지정할 수 있습니다.
여기까지 오면 기본적인 Jacoco 설정은 끝이다.
마지막으로, 리포트와 검증 task에서 테스트하지 않아도 될 클래스들을 제외하는 방법을 알아보자.
4. 제외하기
실제 테스트가 필요없는 부분들을 Jacoco에서 제외할 필요가 있다.
내가 생각한 제외 범위는 다음과 같다.
- @SpringBootApplication 붙은 클래스
- Swagger, JpaAuditing, PasswordEncoding 설정을 위한 Config 클래스
- Dto 클래스
- Request, Response 클래스
물론, 상기한 클래스 외에도 더 있을 수도 있다. QueryDSL을 추가했을 때 생기는 Q 도메인 클래스라던가 Interceptor, 상황에 따라선 Exception 클래스도 제외할 수 있다. 정답은 없다.
4-1. 리포트(jacocoTestReport
)에서 제외
afterEvaluate
설정에서 디렉토리 경로를 통해 제외한다.
*
와 ?
을 통해서 제외할 수 있다.
*
일 땐, 0개 이상의 문자와 일치, **
이면 0개 이상의 디렉토리와 일치한다는 문법이다.
?
은 단일 문자와 일치한다는 뜻이다.
// 리포트 작성 시 특정 파일 제외
afterEvaluate {
classDirectories.setFrom(
files(classDirectories.files.collect {
fileTree(dir: it, excludes: [
'**/ProjectApplication*',
'**/*Request*',
'**/*Response*',
'**/Hello*',
'**/Swagger*',
'**/PasswordEncoderConfig*',
'**/JpaAuditingConfig*'
// ...
])
})
)
}
4-2. 검증(jacocoTestCoverageVerification
)에서 제외
exclude
설정을 통해 검증 단계에서 제외한다.
한가지 다른 부분은 리포트 단계에서와는 달리 패키지 + 클래스 경로를 통해 제외시켜야 한다.
// 검증 단계에서 파일 제외
excludes = [
'**.*ProjectApplication*',
'**.*Request*',
'**.*Response*',
'**.*Hello*',
'**.*Swagger*',
'**.*PasswordEncoder*',
'**.*JpaAuditing*'
]
✅ 제외하는 방법은 사용자 어노테이션을 통해 진행하는 방법도 있다.
4-3. Lombok 제외
Lombok이 자동 생성해주는 @Getter, @Builder와 같은 코드도 제외시키는 것이 좋다.
방법은 프로젝트 루트 경로에 lombok.config
를 만들고, Lombok이 달려있는 모든 클래스에 @Generated 어노테이션을 추가하는 설정을 다음과 같이 해준다.
lombok.addLombokGeneratedAnnotation = true
5. 테스트 진행 후 결과 확인
이제 모든 작업이 끝났으니 테스트를 진행한다.
프로젝트 폴더에서 터미널을 이용해 ./gradlew test
명령어를 통해 진행해도 되고, 인텔리제이의 우측 상단에 Gradle 탭에서 Tasks - verification - test를 클릭해도 된다.
결과를 확인해보자.
리포트를 저장해놓은 경로에 가보면 다음과 같이 html 파일이 있다.
열어보면 다음과 같이 테스트 결과를 확인할 수 있다.
아직 service, repository 테스트를 많이 작성하지 않아서 결과가 참담하다…
최소 80%를 넘길 수 있도록 해보겠다.
최종 build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.6'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
// jacoco
id 'jacoco'
}
group = 'com.likelion'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
runtimeOnly 'com.mysql:mysql-connector-j'
implementation 'io.springfox:springfox-swagger-ui:3.0.0'
implementation 'io.springfox:springfox-boot-starter:3.0.0'
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: "0.9.1"
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
//tasks.named('test') {
// useJUnitPlatform()
//}
test {
useJUnitPlatform() // JUnit5를 사용하기 위한 설정
finalizedBy 'jacocoTestReport' // Test 이후 커버리지가 동작하도록 finalizedBy 추가
}
jacoco {
toolVersion = '0.8.8'
}
// 바이너리 커버리지 결과를 사람이 읽기 좋은 형태의 리포트로 저장
jacocoTestReport {
dependsOn test
reports {
html.enabled true
xml.enabled true
csv.enabled true
// html 파일 위치 지정
html.destination file('build/reports/myReport.html')
}
// 리포트 작성 시 특정 파일 제외
afterEvaluate {
classDirectories.setFrom(
files(classDirectories.files.collect {
fileTree(dir: it, excludes: [
'**/ProjectApplication*',
'**/*Request*',
'**/*Response*',
'**/Hello*',
'**/Swagger*',
'**/PasswordEncoderConfig*',
'**/JpaAuditingConfig*'
// ...
])
})
)
}
finalizedBy 'jacocoTestCoverageVerification'
}
// 내가 원하는 커버리지 기준을 만족하는지 확인해 주는 task
// 점수 평가 및 검증 단계
jacocoTestCoverageVerification {
violationRules {
rule {
enabled = true // 활성화
element = 'CLASS' // 클래스 단위로 커버리지 체크
// includes = []
// 라인 커버리지 제한을 80%로 설정
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.80
}
// 브랜치 커버리지 제한을 80%로 설정
limit {
counter = 'BRANCH'
value = 'COVEREDRATIO'
minimum = 0.80
}
// 빈 줄을 제외한 코드의 라인수를 최대 200라인으로 제한
limit {
counter = 'LINE'
value = 'TOTALCOUNT'
maximum = 200
}
excludes = [
'**.*ProjectApplication*',
'**.*Request*',
'**.*Response*',
'**.*Hello*',
'**.*Swagger*',
'**.*PasswordEncoder*',
'**.*JpaAuditing*'
]
}
}
}
Leave a comment