[공부정리] 테스트 코드 커버리지 분석 도구 Jacoco 적용

jeyong·2024년 1월 31일
0

공부 / 생각 정리  

목록 보기
17/120
post-custom-banner

이번에 다룰 주제는 테스트 코드 커버리지 분석 도구 Jacoco이다.

테스트 코드를 작성하며 내가 테스트 코드를 잘 작성하고있는지에 대한 의구심이 생겼다. 또한 테스트 케이스를 얼마나 작성해야하는지 궁금하게 되었다. 해당 문제를 해결하기 위해서 검색을 하던중 Jacoco라는 도구를 발견하였다. 해당 도구를 현재 진행중인 프로젝트에 적용하여 작성한 테스트 코드들이 얼마나 잘 작성되었는지 확인해보기로 하였다.

1. Jacoco

1-1. 코드 커버리지

코드 커버리지는 개발된 소프트웨어의 테스트 케이스가 소스 코드의 어느 정도를 실행하였는지를 수치로 나타내는 지표이다. 이 지표를 통해 개발자는 테스트가 충분히 이루어졌는지, 어떤 부분이 누락되었는지를 파악할 수 있다. 일반적으로 코드 커버리지는 구문(Statement), 조건(Condition), 결정(Decision) 커버리지로 나뉘며, 이는 테스트가 코드의 어떤 구조를 얼마나 커버하는지에 따라 구분된다.

  • 구문 커버리지(Statement Coverage): 코드의 각 구문이 최소 한 번 이상 실행되었는지를 측정한다.
  • 조건 커버리지(Condition Coverage): 조건문 내의 각 조건이 참과 거짓을 모두 경험했는지를 측정한다.
  • 결정 커버리지(Decision Coverage): 조건문의 결정 결과가 참과 거짓을 모두 경험했는지를 측정한다.
    코드 커버리지 측정은 테스트의 질을 객관적으로 평가하고, 누락된 테스트 케이스를 찾아내는 데 도움을 준다.

1-2. Jacoco

JaCoCo(Java Code Coverage)는 Java 프로그램을 위한 코드 커버리지 분석 도구이다. 이 도구는 개발 과정에서 테스트 케이스가 소스 코드의 어느 부분을 실행했는지 시각적으로 보여주며, 개발자가 코드의 테스트 커버리지를 쉽게 파악하고 향상시킬 수 있도록 지원한다.

Jacoco의 주요 장점

  • 다양한 커버리지 측정: 라인, 브랜치, 클래스, 메소드 별 커버리지 제공.
  • 리포트 생성: HTML, XML, CSV 포맷으로 커버리지 리포트를 생성, 이를 통해 테스트 커버리지 결과를 쉽게 확인하고 공유할 수 있다.
  • 통합 용이성: Maven, Gradle 등 다양한 빌드 도구와의 통합이 용이하며, CI/CD 파이프라인에도 쉽게 통합하여 자동화된 테스트 커버리지 측정 및 모니터링을 구현할 수 있다.
  • 커버리지 기준 설정: 프로젝트의 요구사항에 따라 커버리지 기준을 설정할 수 있으며, 기준에 미달할 경우 빌드 실패로 지정하여 코드의 품질을 강제할 수 있다.

JaCoCo를 사용함으로써 개발자는 테스트의 빈틈을 쉽게 발견하고, 코드의 품질을 지속적으로 개선할 수 있다. 또한, 팀 내에서 코드 커버리지 목표를 설정하고 이를 달성하기 위한 노력을 강화할 수 있는 기회를 제공한다.

2. 환경 설정

Jacoco는 프로젝트에 간단하게 적용할 수 있다.

build.gradle

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중 일부이다. 아래에서 자세하게 설명하겠다.

2-1. plugin 추가

id 'jacoco' // jacoco

jacoco 플러그인을 추가하였다.

2-2. test 실행 후 자동으로 jacocoTestReport 실행

test {
	useJUnitPlatform()
	// finalizedBy jacocoTestReport // Generates report after tests are run
}

finalizedBy jacocoTestReport는 test를 실행 후, jacocoTestReport를 실행하도록 할 수있다. 나는 Jacoco 도입 이후에 깃허브 액션을 통한 CI를 구축할때 Test과정과 jacocoTestReport 과정이 병렬적으로 진행되도록 할 계획이기 때문에 제외하였다.

2-3. jacoco 버전과 분석 결과 경로 설정

jacoco {
    toolVersion = "0.8.10"
    reportsDirectory = layout.buildDirectory.dir('jacocoReport')
}

위와 같이 분석 리포트가 생성되는 경로를 지정할 수 있다.

build/jacocoReport 라는 디렉토리가 생성되고 해당 디렉토리 안에 리포트가 생성된다.

2-4. jacocoTestReport 설정

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가 실행되도록 하였다.

2-5. 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와 다른 방식으로 커버리지 검증에서 제외할 경로도 추가할 수 있다.

커버리지 기준을 만족하지 못할경우, 사진과 같이 오류가 발생한다.

2-6. lombok.config 설정을 통한 lombok 제외

위에 과정까지만 수행할 경우, lombok을 통해 생성한 @builder와 @entitiy등이 coverage에 포함대상되어 정확한 코드 커버리지 측정이 불가능하다.

lobok.config

lombok.addLombokGeneratedAnnotation = true

lombok.addLombokGeneratedAnnotation = true를 통해서 lombok을 통해 생성한 어노테이션들을 검증 대상에서 제외할 수 있다.

3. Jacoco Report

3-1. Jacoco Report 생성

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가 생성된다.

3-2. Jacoco Report 확인

Jacoco Report를 html로 생성하였을 경우, 직접 확인이 가능하다.

라인 커버리지와 브랜치 커버리지를 눈으로 확인할 수 있다.

상세 화면에 들어갈 경우, 사진과 같이 같이 라인 커버리지를 확인할 수 있다.

4. 마무리

Jacoco를 통해 코드 커버리지를 측정해보았는데, 라인 커버리지와 브랜치 커버리지를 둘다 적용하였음에도 큰 문제없이 프로젝트에 적용할 수 있었던 것을 보아서는 평소에 테스트 코드를 잘 작성하고 있었던 것 같다. 앞으로는 프로젝트를 시작할때부터 Jacoco를 적용하여 계속해서 코드 커버리지를 측정하며 진행할 생각이다. 코드 커버리지가 80%가 넘도록 하는것이 좋은 코드의 기준이라는 것을 명심하고 앞으로도 열심히 테스트 코드를 작성할 생각이다.

profile
노를 젓다 보면 언젠가는 물이 들어오겠지.
post-custom-banner

0개의 댓글