본문 바로가기

백엔드 개발일지

[Spring] Code Coverage 측정을 위한 JaCoCo 적용하기

1. JaCoCo란

JaCoCo는 Java code coverage를 측정하는 오픈소스 라이브러리입니다.

JaCoCo는 Line, Branch Coverage를 제공하며 테스트를 실행한 후, 결과를 html/csv/xml 파일을 통해 보기 좋게 시각화를 해줍니다.

 

Code Coverage
소프트웨어의 테스트를 논할 때 얼마나 테스트가 충분한가를 나타내는 지표

 

 

 

2. JaCoCo 적용하기

* 해당 포스트에 대한 코드 내용은 여기서 확인할 수 있습니다.

(https://github.com/depromeet/14th-team5-BE/pull/7)

 

개발 환경: SpringBoot 3.1.5, Java 17, Gradle

server: 루트 프로젝트
├── config: 프로젝트 설정 관리
│   ├── build
│   ├── build.gradle
│   └── src
├── member: Member 도메인
│   ├── build
│   ├── build.gradle
│   └── src
├── build
├── build.gradle
├── gradle
│   └── wrapper
├── gradlew
├── gradlew.bat
└── settings.gradle

 

저희 프로젝트는 현재 멀티 모듈로 구성되어 있으며, JaCoCo는 config 모듈에 설정했습니다.

 

 

1) JaCoCo 플러그인 추가

  • root/settings.gradle
// ...

subprojects {
	group = 'com.oing'
	version = '0.0.1-SNAPSHOT'
	sourceCompatibility = '17'

	apply plugin: 'java'
	apply plugin: 'org.springframework.boot'
	apply plugin: 'io.spring.dependency-management'
	apply plugin: 'jacoco' // 추가

// ...

 

현재 프로젝트의 구조는 멀티모듈이고 모든 모듈의 테스트에 JaCoCo를 적용할 예정이므로 JaCoCo 플러그인을 subprojects 블록 안에 추가합니다.

 

JaCoCo Task 추가된 모습

 

gradle을 새로 고침하면 의존성이 추가되면서 사진과 같이 모든 모듈의 Tasks/verification에 JaCoCo의 Task가 추가됩니다.

여기서 추가된 Task의 역할은 다음과 같습니다.

  • jacocoTestCoverageVerification: 바이너리 커버리지 결과를 사람이 읽기 좋은 형태의 리포트로 저장합니다. html 파일로 생성해 사람이 쉽게 눈으로 확인할 수도 있고, SonarQube 등으로 연동하기 위해 xml, csv 같은 형태로도 리포트를 생성할 수 있습니다.
  • jacocoTestReport: 내가 원하는 커버리지 기준을 만족하는지 확인해 주는 task입니다. 사용자가 별도로 커버리지 기준을 설정하고 싶다면 해당 task에 설정합니다. test task처럼 Gradle 빌드의 성공/실패로 결과를 보여줍니다.

 

 

2) JaCoCo 플러그인 설정

jacocoTestCoverageVerification과 jacocoTestReport에 대한 설정을 하기 전, 먼저 JaCoCo 플러그인 설정을 합니다.

JaCoCo 플러그인은 자동으로 모든 Test 타입의 task에 JacocoTaskExtension을 추가하고, test task에서 그 설정을 변경할 수 있게 합니다.

 

  • root/settings.gradle -> toolVersion 설정
subprojects {
	// ...

	apply plugin: 'java'
	apply plugin: 'org.springframework.boot'
	apply plugin: 'io.spring.dependency-management'
	apply plugin: 'jacoco'

	configurations {
		compileOnly {
			extendsFrom annotationProcessor
		}
	}

	jacoco {
		toolVersion = '0.8.8' // 추가
	}
    
    // ...
  }
  
  // ...

 

toolVersion은 사용할 JaCoCo의 JAR 버전입니다. 저는 0.8.8 버전을 사용했습니다.

 

 

 

3) jacocoTestReport Task 설정

  • root/settings.gradle
jacocoTestReport {
	reports{
    	    html.required.set(true)
            xml.required.set(true)
            csv.required.set(true)
            html.destination file("$buildDir/reports/jacoco/index.html")
            xml.destination file("$buildDir/reports/jacoco/index.xml")
            csv.destination file("$buildDir/reports/jacoco/index.csv")
}

 

이 안에서는 테스트를 수행한 결과를 리포트 파일로 저장하는 태스크의 설정을 합니다.

 

jacocoTestReport task는 리포트 파일을 html, xml, csv 형태로 저장합니다. 읽기 편한 파일 형식인 html은 true로 설정을 하고 추후 소나큐브를 연동할 예정이면 csv, xml까지 true로 설정을 합니다.

(저희 프로젝트는 추후 소나큐브 연동할 예정이라 세 가지 형태 모두 true로 설정했습니다)

 

또한 파일 형식에 따라 저장 경로를 다르게 하고 싶다면 destination file('디렉토리 경로')을 통해 특정 경로에 저장할 수 있습니다.

 

 

 

4) jacocoTestCoverageVerification Task 설정

jacocoTestCoverageVerification Task 안에서는 violationRules 을 통해 최소 드 커버리지 기준을 설정하여 해당 테스트가 커버리지를 만족하는 지 확인할 수 있습니다.

 

  • root/settings.gradle
jacocoTestCoverageVerification {
	violationRules {
    	rule {
            enabled = true
            element = 'CLASS'
            
            // 라인 커버리지 설정
            limit {
            	counter = 'LINE'
                value = 'COVEREDRATIO'
                minimum = 0.70
            }
            
            // 브랜치 커버리지 설정
            limit {
            	counter = 'BRANCH'
                value = 'COVEREDRATIO'
                minimum = 0.70
            }
            
            // 커버리지 측정 시, 제외할 클래스 지정
            excludes = [
            
            ]
        }
    }
}

 

  • enable: 해당 rule의 활성화 여부를 true/false 값으로 나타내며 디폴트는 true입니다.
  • element: 룰을 체크하는 단위를 나타냅니다. -> 현재 예시에서는 클래스 단위로 라인과 브랜치 커버리지를 체크합니다.
    • BUNDLE (default): 패키지 번들
    • PACKAGE: 패키지
    • CLASS: 클래스
    • SOURCEFILE: 소스파일
    • METHOD: 메소드
  • counter: 커버리지 측정의 최소 단위를 나타냅니다.
    • LINE: 빈 줄을 제외한 실제 코드의 라인 수
    • BRANCH: 조건문 등의 분기 수
    • CLASS: 클래스 수
    • METHOD: 메소드 수
    • INSTRUCTION (default): Java 바이트코드 명령 수
    • COMPLEXITY: 복잡도
  • value: 측정한 커버리지를 어떻게 보여줄 것인지 나타냅니다.
    • TOTALCOUNT: 전체 개수
    • MISSEDCOUNT: 커버되지 않은 개수
    • COVEREDCOUNT: 커버된 개수
    • MISSEDRATIO: 커버되지 않은 비율. 0부터 1 사이의 숫자로, 1이 100%입니다.
    • COVEREDRATIO (default): 커버된 비율. 0부터 1 사이의 숫자로, 1이 100%입니다.

 



5) 커버리지 측정 및 리포트 파일에서 제외할 파일 설정

 

일반적으로 커버리지를 할 필요가 없는 코드들도 있습니다. excludes를 통해 커버리지 측정할 때 제외할 클래스를 지정할 수 있습니다. 이때 패키지 레벨의 경로로 지정해야 하며 경로에 와일드카드(*과 ?)를 사용할 수 있습니다. 

 

  • 커버리지 측정에서 제외할 클래스 설정
violationRules {
    def Qdomains = []
    for (qPattern in '*.QA'..'*.QZ') { // qPattern = '*.QA', '*.QB', ... '*.QZ'
    	Qdomains.add(qPattern + '*')
    }
    
    rule {
    	enabled = true
        element = 'CLASS'
        
        // ...
        
        excludes = [
            "**.*Application*", // Application 파일 제외
            "**.*Config*" // Config 파일 제외
            ] + Qdomains // QueryDsl을 사용하면 자동으로 생성되는 구현체인 Q*.class 파일 제외
        }
    }
}

 

위와 같이 jacocoTestCoverageVerification Task 안에서 excludes를 설정하면 Application, Config 파일과 QueryDsl을 사용하면 생기는 Q*.class 파일까지 제외합니다.

 

 

 

  • 리포트를 작성할 때 제외하는 설정

리포트를 작성할 때 제외하기 위해 jacocoTestReport task 안에 설정을 추가합니다.

 

jacocoTestReport {
    // ...
    
    def Qdomains = []
    for (qPattern in '*.QA'..'*.QZ') { // qPattern = '*.QA', '*.QB', ... '*.QZ'
    	Qdomains.add(qPattern + '*')
    }
    
    afterEvaluate {
    	classDirectories.setFrom(
            files(classDirectories.files.collect {
                fileTree(dir: it, excludes: [
                    // 측정 안하고 싶은 패턴
                    "**/*Application*",
                    "**/*Config*"
                    // Querydsl 관련 제거
                    ] + Qdomains)
                })
            )
        }
		// ....
   }

 

  • afterEvaluate: 프로젝트가 평가된 후 실행됩니다.
  • classDirectories: 커버리지가 리포트로 작성할 소스 파일을 나타냅니다. fileTree 안에 지정한 측정 안할 패턴들의 파일들을 리포트에서 제외합니다.

 

 

 

6) 커버리지 측정 및 리포트 파일에서 제외할 파일 설정

마지막으로 test 태스크만 실행해도 jacoco가 자동으로 실행되도록 하기 위해 finalizeBy를 이용한 코드를 추가해줍니다.

finalizeBy는 해당 태스크가 종료된 후 뒤에 적힌 태스크를 자동으로 실행합니다.

  • test task 안에 finalizedBy 'jacocoTestReport' 추가: test 태스크가 끝나면 자동으로 jacocoTestReport 태스크를 실행합니다.
  • jacocoTestReport task 안에 finalizedBy 'jacocoTestCoverageVerification' 추가: jacocoTestReport 태스크가 끝나면 자동으로 jacocoTestCoverageVerification 태스크를 실행합니다.
subprojects {
    // ...
    apply plugin: 'java'
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'
    apply plugin: 'jacoco'
    
    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
    }
    
    tasks.named('test') {
    	useJUnitPlatform()
        finalizedBy 'jacocoTestReport' // 추가: test -> jacocoTestReport
    }
    
    jacoco {
        toolVersion = '0.8.8'
    }
    
    jacocoTestReport {
    	reports{
            html.required.set(true)
            xml.required.set(true)
            csv.required.set(true)
            html.destination file("$buildDir/reports/jacoco/index.html")
            xml.destination file("$buildDir/reports/jacoco/index.xml")
            csv.destination file("$buildDir/reports/jacoco/index.csv")
        }
        
        def Qdomains = []
        for (qPattern in '*.QA'..'*.QZ') { // qPattern = '*.QA', '*.QB', ... '*.QZ'
            Qdomains.add(qPattern + '*')
        }
        
        afterEvaluate {
        	classDirectories.setFrom(
            	files(classDirectories.files.collect {
                	fileTree(dir: it, excludes: [
                    	// 측정 안하고 싶은 패턴
                        "**/*Application*",
                        "**/*Config*",
                        "**/config/*",
                        "**/dto/*"
                        // Querydsl 관련 제거
                        ] + Qdomains)
                   })
              )
        }
        // 추가: jacocoTestReport -> jacocoTestCoverageVerification
        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.70
                }
                
                // 브랜치 커버리지 설정
                limit {
                    counter = 'BRANCH'
                    value = 'COVEREDRATIO'
                    minimum = 0.70
                }
                
                excludes = [
                        "**.*Application*",
                        "**.*Config*",
                        "**.config.*",
                        "**.dto.*"
                ] + Qdomains
            }
        }
    }
    
    // ...
    }
}

// ...

 

 

 

이렇게 설정을 하면 ./gradlew clean test만 해도 자코코 테스트까지 자동으로 실행됨을 알 수 있습니다.

./gradlew clean test
config모듈에 대한 리포트 결과

 

jacoco 커버리지 범위에서 제외하지 않은 파일에 한해 커버리지를 측정하고 위의 사진처럼 사용자가 설정한 기준치에 못미치는 파일에 대해 커맨드 창에서는 로그를 남깁니다. 그리고 리포트 파일에서는 눈에 보기 좋게 시각화를 해서 결과를 보여줍니다.