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에서 권장해준 버전을 설정해주었다.

image

툴 버전(toolVersion) 말고도 repostsDir = file()을 통해 Jacoco 리포트 결과물 디렉토리를 설정할 수 있다. 하지만 ,아래의 jacocoTestReport task에서 설정해 줄 수 있으므로 따로 적지 않는다.

플러그인을 추가하면 다음과 같이 jacocoTestReportjacocoTestCoverageVerification task가 gradle verification항목에 추가된다.

image

3-2. task 설정 - jacocoTestReport

플러그인을 추가하니, jacocoTestReportjacocoTestCoverageVerification 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의 설정값들을 살펴보자.

  1. enabled
    • 테스트 검증을 위한 해당 rule을 활성화할 것인지를 나타낸다. default는 true이다.
  2. element
    • 테스트를 적용할 대상 혹은 테스트 측정 단위로 6개 유형이 존재한다.
    • Default값은 BUNDLE
    • BUNDLE : 패키지 번들(프로젝트 모든 파일을 합친 것)
    • ✅CLASS : 클래스
    • GROUP : 논리적 번들 그룹
    • METHOD : 메서드
    • PACKAGE : 패키지
    • SOURCEFILE : 소스 파일
  3. limit
    • rule의 상세 설정
  4. counter
    • 커버리지 측정의 최소 단위로 마찬가지로 6개의 유형이 존재한다.
    • Default는 INSTRUCTION : 가장 작은 측정 방식으로, Java 바이트코드를 읽는다.
    • ✅BRANCH : 조건문(if, switch) 등의 분기 수에 대해 측정을 진행한다.
    • ✅LINE : 라인이 한 번이라도 실행되면 실행된 것으로 간주, 빈 줄은 제외한다.
    • CLASS : 클래스 내부의 메서드가 한 번이라도 실행된다면 실행된 것으로 간주한다.
    • COMPLEXITY : 코드 복잡도
    • METHOD : 메서드가 한 번이라도 실행되면 실행된 것으로 간주한다.
  5. value
    • 커버리지 측정을 보여주는 방식을 정하는 부분이다.
    • ✅Default는 COVEREDRATIO : 커버된 비율, 0부터 1사이의 숫자로 표현한다.
    • COVEREDCOUNT : 커버된 개수
    • MISSEDCOUNT : 커버되지 않은 개수
    • MISSEDRATIO : 커버되지 않은 비율, 0부터 1사이의 숫자로 표현한다.
    • TOTALCOUNT : 전체 개수
  6. minimum / maximum
    • jacocoTestCoverageVerification의 성공 여부를 결정하는 기준이다.
    • 예를 들어, minimum 0.8을 입력하면 80% 이상을 통과해야 성공이다.
    • maximum의 경우 코드의 라인 수를 제한할 때 사용한다.
  7. 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를 클릭해도 된다.

image

결과를 확인해보자.

리포트를 저장해놓은 경로에 가보면 다음과 같이 html 파일이 있다.

image

열어보면 다음과 같이 테스트 결과를 확인할 수 있다.

아직 service, repository 테스트를 많이 작성하지 않아서 결과가 참담하다…

최소 80%를 넘길 수 있도록 해보겠다.

image

최종 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*'

			]
		}
	}
}



References

Leave a comment