JiwonDev

[์ฝ”๋“œ๋ถ„์„๋„๊ตฌ]#2 Jacoco ์ ์šฉํ•˜๊ธฐ

by JiwonDev

์ธ๋„ค์ผ์€ ๋”ฑํžˆ ๋„ฃ์„๊ฒŒ ์—†์–ด์„œ ์  ํ‚จ์Šค๋ฅผ ๋„ฃ์—ˆ๋‹ค. ๊ทธ๋ƒฅ CI ๋„๊ตฌ๋ผ๋Š” ๊ด€์ ์—์„œ..?

์ฐธ์กฐํ•œ ๋ ˆํผ๋Ÿฐ์Šค

JaCoCo is a free code coverage library for Java, which has been created by the EclEmma team based on the lessons learned from using and integration existing libraries for many years
- JaCoCo

Jacoco๋Š” Java Code์˜ ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์ฒดํฌํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋˜๋Š” ์˜คํ”ˆ์†Œ์Šค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค.

 

 

# Jacoco์˜ ํŠน์ง•

์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€์— ์ต์ˆ™ํ•˜์ง€ ์•Š์„ ๋•Œ๋Š” ์ปค๋ฒ„๋ฆฌ์ง€ ๊ธฐ์ค€์„ ๋‚ฎ์ถฐ์„œ ์‚ฌ์šฉํ•˜๋‹ค๊ฐ€ ์ ์  ๊ธฐ์ค€์„ ๋†’์ด๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•œ๋‹ค. Jacoco๋Š” ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ํ‰๊ฐ€ํ•˜๊ณ  ํ”„๋กœ์ ํŠธ ๋นŒ๋“œ ์„ฑ๊ณต/์‹คํŒจ๋ฅผ ๊ฒฐ์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ค์–ด์ค€๋‹ค.

  • ๋ผ์ธ ์ปค๋ฒ„๋ฆฌ์ง€(๋ชจ๋“  ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋˜๋Š”๊ฐ€)์™€ ๋ธŒ๋ Œ์น˜ ์ปค๋ฒ„๋ฆฌ์ง€(๋ชจ๋“  ์กฐ๊ฑด๋ถ„๊ธฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๊ฐ€)๋ฅผ ์ œ๊ณต.
  • ํ…Œ์ŠคํŠธ์ฝ”๋“œ๋ฅผ ๋Œ๋ฆฌ๊ณ  ์ปค๋ฒ„๋ฆฌ์ง€ ๊ธฐ์ค€์„ ๋งŒ์กฑํ•˜๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ๊ฒฐ๊ณผ๋ฅผ ์ •๋ฆฌ๋œ ํŒŒ์ผ(html, xml, csv๋“ฑ)์œผ๋กœ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋‹ค.

Jacoco๋ฅผ ํ†ตํ•ด ์ฝ”๋“œ์ปค๋ฒ„๋ฆฌ์ง€ ๊ฒฐ๊ณผ๋ฅผ csv, xml๋กœ ์ €์žฅํ•˜๋ฉด ์ด๋ฅผ ์†Œ๋‚˜ํ๋ธŒ(SonarQube) ๊ฐ™์€ ๋„๊ตฌ์— ์ „๋‹ฌํ•ด ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ ์ปค๋ฐ‹์„ฑ๊ณต/์‹คํŒจ๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ์‹์œผ๋กœ ์‚ฌ์šฉ๋œ๋‹ค. ์ฐธ๊ณ ๋กœ html์„ ์‚ฌ์šฉํ•˜๋ฉด ์–ด๋А ๋ถ€๋ถ„์ด ๋ฌธ์ œ์ธ์ง€ ๊ฐœ๋ฐœ์ž๊ฐ€ ํ•œ๋ˆˆ์— ํŒŒ์•…ํ•˜๊ธฐ ์‰ฝ๊ฒŒ ๋ณด๊ณ ์„œ๋กœ ๋งŒ๋“ค์–ด์ค€๋‹ค.

 


# Jacoco ์ ์šฉํ•˜๊ธฐ

Java + SpringBoot + Gradle๋กœ ์ด๋ฃจ์–ด์ง„ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•ด๋ณด์ž. ์Šคํ”„๋ง๋ถ€ํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด build.gradle ์„ค์ •๋งŒ์„ ์ด์šฉํ•ด์„œ ์‰ฝ๊ฒŒ ์ ์šฉ๊ฐ€๋Šฅํ•˜๋‹ค.

 

1. Jacoco๋ฅผ ์ „์ฒด ํ”„๋กœ์ ํŠธ๋ฅผ ํ•˜๋‚˜๋กœ ์ผ๊ด„์ ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ดํ•˜๋ฉด ๋œ๋‹ค.

//build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '2.3.1.RELEASE'
...
id 'jacoco' // ์ถ”๊ฐ€!
}
jacoco {
// JaCoCo ๋ฒ„์ „
toolVersion = '0.8.6'
// ํ…Œ์ŠคํŠธ๊ฒฐ๊ณผ ๋ฆฌํฌํŠธ๋ฅผ ์ €์žฅํ•  ๊ฒฝ๋กœ
// ๋ณ€๊ฒฝํ•˜๊ณ ์ž ํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ž‘์„ฑ default๋Š” "${project.reporting.baseDir}/jacoco"
reportsDir = file("$buildDir/customJacocoReportDir")
}

 

2. ๊ทธ๊ฒŒ ์•„๋‹ˆ๋ผ ๊ฐ ๋ชจ๋“ˆ๋ณ„๋กœ ๋”ฐ๋กœ Jacoco๋ฅผ ์ ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด ์„ค์ •ํ•ด์ฃผ์ž.

// build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '2.3.1.RELEASE'
...
}
allprojects {...}
subprojects {
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'org.asciidoctor.convert'
apply plugin: 'jacoco'// ์—ฌ๊ธฐ์— ์ถ”๊ฐ€!
...
}
jacoco {...}

 

๊ทธ๋Ÿฌ๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ๊ฐ ๋ชจ๋“ˆ์˜ Gradle ๋””๋ ‰ํ† ๋ฆฌ์— Jacoco ์˜์กด์„ฑ์ด ์ถ”๊ฐ€๋œ๋‹ค.

  • jacocoTestReport : ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์ง„ํ–‰ํ•˜๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ์‚ฌ๋žŒ์ด ์ฝ๊ธฐ ์ข‹์€ ๋ฆฌํฌํŠธ๋กœ ์ €์žฅํ•ด์ฃผ๋Š” ๋„๊ตฌ
  • jacocoTestCoverageVerification : ์›ํ•˜๋Š” ์ปค๋ฒ„๋ฆฌ์ง€ ๊ธฐ์ค€์„ ๋งŒ์กฑํ•˜๋Š”์ง€ ํ™•์ธํ•ด์ฃผ๋Š” ๊ฒ€์ฆ ๋„๊ตฌ
๊ฐ ๋ชจ๋“ˆ๋ณ„๋กœ jacoco ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ์ถ”๊ฐ€

์ด ์„ค์ •์€ build.gradle์—์„œ ํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ, ํ•œ๊ฐ€์ง€ ์ฃผ์˜ํ• ์ ์€ jacoco๋Š” ํ…Œ์ŠคํŠธ ์ดํ›„ ์ ์šฉ๋˜๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ์ด๋ผ๋Š” ์ ์ด๋‹ค. ์ฆ‰ [ Junit Test -> jacocoTestReport -> jacoco...CoverageVerification ] ์ˆœ์œผ๋กœ ์‹คํ–‰๋˜์–ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ž˜์™€ ๊ฐ™์ด ํ…Œ์ŠคํŠธ ์„ค์ •์„ ๊ณ ์ •์‹œ์ผœ์ฃผ์ž. ์ด๋ฅผ ์ƒ๋žตํ•˜๊ณ  ์•ˆํ•ด์ฃผ๋ฉด ์ด์ „ ํ…Œ์ŠคํŠธ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜๋ณตํ•ด์„œ ์ฝ์–ด ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๊ณ„์† ํ†ต๊ณผ ๋ชปํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

//build.gradle
test {
useJUnitPlatform() // JUnit5๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์„ค์ •
finalizedBy 'jacocoTestReport' // Test ์ดํ›„ ์ปค๋ฒ„๋ฆฌ์ง€๊ฐ€ ๋™์ž‘ํ•˜๋„๋ก finalizedBy ์ถ”๊ฐ€
}
jacoco {
toolVersion = '0.8.6'
}
jacocoTestReport {
reports {
html.enabled true
csv.enabled true
xml.enabled false
}
finalizedBy 'jacocoTestCoverageVerification' // ์ปค๋ฒ„๋ฆฌ์ง€ ์ž‘๋™ ์ดํ›„ ๊ฒ€์ฆํ•˜๋„๋ก ์„ค์ •
}

 


# jacocoTestReport (์ปค๋ฒ„๋ฆฌ์ง€ ์ธก์ •)

build.gradle์—์„œ jacocoTestReport {...}์— ์„ค์ • ๊ฐ€๋Šฅํ•˜๋‹ค. ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ๊ฒฐ๊ณผ๋ฅผ ์–ด๋–ป๊ฒŒ ์ €์žฅํ• ์ง€ ์„ค์ •ํ•œ๋‹ค.

//build.gradle
...
jacoco {
toolVersion = '0.8.6'
}
jacocoTestReport {
reports {
csv.enabled true // csv ์„ค์ •
xml.enabled false // xml ๋ฏธ์„ค์ •
html.enabled true // html ์„ค์ •
// ํŒŒ์ผ ํ˜•์‹์— ๋”ฐ๋ผ ์ €์žฅ๊ฒฝ๋กœ๋ฅผ ๋”ฐ๋กœ ์ง€์ •ํ•ด์ค„์ˆ˜๋„ ์žˆ๋‹ค.
html.destination file('build/reports/myReport.html')
...
}
}

 

๋ฆฌํฌํŠธ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ ํŠน์ • ํŒŒ์ผ์„ ์ œ์™ธํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด ์ž‘์„ฑํ•˜๋ฉด ๋œ๋‹ค.

jacocoTestReport {
reports {
html.enabled true
csv.enabled true
xml.enabled false
}
def Qdomains = []
for (qPattern in '**/QA'..'**/QZ') { // qPattern = '**/QA', '**/QB', ... '*.QZ'
Qdomains.add(qPattern + '*') // ~/QA* , QA๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ชจ๋“  ํด๋ž˜์Šค
}
// ์—ฌ๊ธฐ๋ถ€ํ„ฐ
afterEvaluate {
classDirectories.setFrom(
files(classDirectories.files.collect {
fileTree(dir: it, excludes: [] + Qdomains)
})
)
}
finalizedBy 'jacocoTestCoverageVerification'
}
  • afterEvaluate๋Š” gradle์˜ ๋นŒ๋“œ ๋ผ์ดํ”„ ์‚ฌ์ดํด์— ๋Œ€ํ•œ ๋ฉ”์„œ๋“œ. ํ”„๋กœ์ ํŠธ๊ฐ€ ํ‰๊ฐ€๋œ ํ›„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค€๋‹ค.
  • classDirectories๋Š” ์ปค๋ฒ„๋ฆฌ์ง€๊ฐ€ ๋ฆฌํฌํŠธ๋กœ ์ž‘์„ฑํ•  ์†Œ์Šค ํŒŒ์ผ์„ ๋งํ•œ๋‹ค. (์—ฌ๊ธฐ์„œ๋Š” setFrom ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์ด๋ฅผ ์„ค์ •)
  • files๋Š” ์ง€์ •๋œ ํŒŒ์ผ์„ ํฌํ•จํ•˜๋Š” ConfigurableFileCollection ํƒ€์ž…์„ ๋ฐ˜ํ™˜.

์ด๋Ÿฐ์‹์œผ๋กœ ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ๋ฆฌํฌํŠธ์— ์ œ์™ธํ•  excludes ๋ชฉ๋ก์„ ์ถ”๊ฐ€๋กœ ์ง€์ •ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.


# jacocoTestCoverageVerification ( ์ ์ˆ˜ํ‰๊ฐ€, ๊ฒ€์ฆ )

์ตœ์†Œ ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๊ณ , ํ†ต๊ณผํ•˜์ง€ ๋ชปํ• ๊ฒฝ์šฐ ํ•ด๋‹น Task๊ฐ€ ์‹คํŒจํ•˜๊ฒŒ ๋œ๋‹ค.

jacoco...verification { violationRules{~} } ๋ฅผ ํ†ตํ•ด ์ปค๋ฒ„๋ฆฌ์ง€ ๊ธฐ์ค€ ๊ทœ์น™์„ ์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • rule { ... } ๊ทœ์น™์„ ์ ๋Š” ๊ณต๊ฐ„. ์—ฌ๋Ÿฌ๊ฐœ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๊ณ  enable ์˜ต์…˜์œผ๋กœ on/off ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • element = ์ ์šฉํ•  ํƒ€์ž… ๋˜๋Š” ๋Œ€์ƒ์„ ์ง€์ •ํ•œ๋‹ค (์ „์ฒด, ํด๋ž˜์Šค, ๋ฉ”์„œ๋“œ, ํ•œ ํŒŒ์ผ ๋“ฑ๋“ฑ..)
    • BUNDLE(๊ธฐ๋ณธ๊ฐ’) : ํŒจํ‚ค์ง€ ๋ฒˆ๋“ค(ํ”„๋กœ์ ํŠธ ๋ชจ๋“  ํŒŒ์ผ์„ ํ•ฉ์นœ ๊ฒƒ)
    • CLASS : ํด๋ž˜์Šค
    • GROUP : ๋…ผ๋ฆฌ์  ๋ฒˆ๋“ค ๊ทธ๋ฃน
    • METHOD : ๋ฉ”์„œ๋“œ
    • PACKAGE : ํŒจํ‚ค์ง€
    • SOURCEFILE : ์†Œ์Šค ํŒŒ์ผ
  • includes, exclude = ์ž‘์„ฑํ•˜๋ฉด ์ œ์™ธ ๋Œ€์ƒ/์ ์šฉ ๋Œ€์ƒ์„ ํด๋ž˜์Šค๋‚˜ ํŒจํ‚ค์ง€ ๊ฒฝ๋กœ๋กœ ํ•œ์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
    - ์ •๊ทœํ‘œํ˜„์‹์ธ * ์™€ ? ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. import ํ•˜๋“ฏ์ด ๋ฆฌ์ŠคํŠธ๋ฅผ ์ ์œผ๋ฉด ๋œ๋‹ค.
jacocoTestCoverageVerification {
violationRules {
rule { // 1๋ฒˆ ๊ทœ์น™
enable = true // ๊ทœ์น™ ํ™œ์„ฑํ™”์—ฌ๋ถ€
element = 'CLASS' // ์ ์šฉ ๋Œ€์ƒ์„ ์ง€์ •ํ•œ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ BUNDLE (์ „์ฒด ํŒจํ‚ค์ง€)
// BUNDLE, CLASS, GROUP(๋…ผ๋ฆฌ์  ๋ฒˆ๋“ค๊ทธ๋ฃน), METHOD, PACKAGE, SOURCEFILE ๋“ฑ
// ํ•ด๋‹น ๊ทœ์น™์˜ ์ ์šฉ๋Œ€์ƒ์„ ํด๋ž˜์Šค, ํŒจํ‚ค์ง€ ๋‹จ์œ„๋กœ ์ง€์ •๊ฐ€๋Šฅ
// includes = [], ๋‹จ include๋Š” ์ƒ๋žตํ•˜๋ฉด ์ „์ฒด ํŒจํ‚ค์ง€์— ์ผ๊ด„ ์ ์šฉ
// excludes = [], ์ œ์™ธํ•  ํŒจํ‚ค์ง€/ํด๋ž˜์Šค๋ฅผ ์ง€์ •
limit {
counter = 'BRANCH' // ์ปค๋ฒ„๋ฆฌ์ง€ ์ข…๋ฅ˜
value = 'COVEREDRATIO' // ๊ฐ’ (์ „์ฒด ๋น„์œจ)
minimum = 0.90 // ์ตœ์†Œ ๊ธฐ์ค€ 90%
}
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.80 // ์ตœ์†Œ ๊ธฐ์ค€ 80%
}
limit {
counter = 'LINE'
value = 'TOTALCOUNT' // ๊ฐ’(์ „์ฒด ๊ฐœ์ˆ˜)
maximum = 200 // ์ตœ์†Œ ๊ธฐ์ค€ 200์ค„ ์ดํ•˜
}
}
// ์—ฌ๋Ÿฌ rule์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
rule { // 2๋ฒˆ ๊ทœ์น™
...
}
}
}

 

  • limit = ์ปค๋ฒ„๋ฆฌ์ง€ ์ธก์ • ์ตœ์†Œ๋‹จ์œ„๋ฅผ ์ง€์ •ํ•œ๋‹ค.
limit {
counter = 'BRANCH' // ์ปค๋ฒ„๋ฆฌ์ง€ ์ข…๋ฅ˜
value = 'COVEREDRATIO' // ๊ฐ’ (์ „์ฒด ๋น„์œจ)
minimum = 0.90 // ์ตœ์†Œ ๊ธฐ์ค€ 90%
}

 

Counter

  • BRANCH : ์กฐ๊ฑด๋ฌธ ๋“ฑ์˜ ๋ถ„๊ธฐ ์ˆ˜
  • CLASS : ํด๋ž˜์Šค ์ˆ˜, ๋‚ด๋ถ€ ๋ฉ”์„œ๋“œ๊ฐ€ ํ•œ ๋ฒˆ์ด๋ผ๋„ ์‹คํ–‰๋œ๋‹ค๋ฉด ์‹คํ–‰๋œ ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผํ•œ๋‹ค.
  • COMPLEXITY : ์ฝ”๋“œ ๋ณต์žก๋„(๋งํฌ ์ฐธ์กฐ)
  • INSTRUCTION(๊ธฐ๋ณธ๊ฐ’) : Java ๋ฐ”์ดํŠธ์ฝ”๋“œ ๋ช…๋ น ์ˆ˜
  • METHOD : ๋ฉ”์„œ๋“œ ์ˆ˜, ๋ฉ”์„œ๋“œ๊ฐ€ ํ•œ ๋ฒˆ์ด๋ผ๋„ ์‹คํ–‰๋œ๋‹ค๋ฉด ์‹คํ–‰๋œ ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผํ•œ๋‹ค.
  • LINE : ๋นˆ ์ค„์„ ์ œ์™ธํ•œ ์‹ค์ œ ์ฝ”๋“œ์˜ ๋ผ์ธ ์ˆ˜, ๋ผ์ธ์ด ํ•œ ๋ฒˆ์ด๋ผ๋„ ์‹คํ–‰๋˜๋ฉด ์‹คํ–‰๋œ ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผํ•œ๋‹ค.

Value

  • COVEREDCOUNT : ์ปค๋ฒ„๋œ ๊ฐœ์ˆ˜
  • COVEREDRATIO(๊ธฐ๋ณธ๊ฐ’) : ์ปค๋ฒ„๋œ ๋น„์œจ, 0๋ถ€ํ„ฐ 1์‚ฌ์ด์˜ ์ˆซ์ž๋กœ [1 = 100%]์ด๋‹ค.
  • MISSEDCOUNT : ์ปค๋ฒ„๋˜์ง€ ์•Š์€ ๊ฐœ์ˆ˜
  • MISSEDRATIO : ์ปค๋ฒ„๋˜์ง€ ์•Š์€ ๋น„์œจ, 0๋ถ€ํ„ฐ 1์‚ฌ์ด์˜ ์ˆซ์ž๋กœ [1 = 100%]์ด๋‹ค.
  • TOTALCOUNT : ์ „์ฒด ๊ฐœ์ˆ˜

Minimum

  • ์œ„์˜ Value๋กœ ์ง€์ •ํ•œ ํƒ€์ž…์˜ ์ตœ์†Œ๊ธฐ์ค€๊ฐ’์„ ์ž…๋ ฅํ•˜๋ฉด ๋œ๋‹ค. jacocoTest์˜ ์„ฑ๊ณต์—ฌ๋ถ€๊ฐ€ ๊ฒฐ์ •๋˜๋Š” ๊ธฐ์ค€๊ฐ’์ด๋ฉฐ ํ‘œ๊ธฐํ•œ ์ž๋ฆฟ ์ˆ˜๋งŒํผ value๊ฐ€ ์„ธ๋ฐ€ํ•˜๊ฒŒ ์ธก์ •๋œ๋‹ค. ์ฆ‰ 0.80์„ ์ž…๋ ฅํ•˜๋ฉด ์†Œ์ˆซ์  ๋‘˜์งธ์ž๋ฆฌ๊นŒ์ง€, 0.8์„ ์ž…๋ ฅํ•˜๋ฉด ์ฒซ์งธ ์ž๋ฆฌ๊นŒ์ง€๋งŒ ์ธก์ •ํ•˜๋ฉฐ ๊ทธ ์ดํ•˜์˜ ๊ฐ’๋“ค์€ ๋ฒ„๋ ค์ง„๋‹ค. (๋ฐ˜์˜ฌ๋ฆผ ์•„๋‹˜)

 


# Test ์„ค์ •ํ•˜๊ธฐ

๋ชจ๋“  ํ…Œ์ŠคํŠธ ํƒ€์ž…์— JacocoTaskExtension์„ ์ถ”๊ฐ€ํ•˜๊ณ , test {...}์—์„œ ์„ค์ •์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด๋ ‡๊ฒŒ ์„ค์ •ํ•œ Jacoco ์ปค๋ฒ„๋ฆฌ์ง€๋Š” ๊ธฐ์ค€์„ ๋งŒ์กฑํ•˜์ง€๋ชปํ•˜๋ฉด ํ”„๋กœ์ ํŠธ๊ฐ€ gradle์— ๋นŒ๋“œ๋˜์ง€ ์•Š๋Š”๋‹ค.

//build.gradle
test { // ๊ฐ ์„ค์ •๊ฐ’์€ ์˜ค๋ฒ„๋ผ์ด๋“œ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค.
jacoco {
enabled = true
destinationFile = file("$buildDir/jacoco/${name}.exec")
includes = []
excludes = []
excludeClassLoaders = []
includeNoLocationClasses = false
sessionId = "<auto-generated value>"
dumpOnExit = true
classDumpDir = null
output = JacocoTaskExtension.Output.FILE
address = "localhost"
port = 6300
jmx = false
}
}
/gradlew --console verbose test ๋ฅผ ์ด์šฉํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์–ด๋–ค ํด๋ž˜์Šค๊ฐ€ ๋ฌธ์ œ์ธ์ง€ ์ฝ˜์†”์— ํ‘œ์‹œํ•ด์ค€๋‹ค.
build/reports/jacoco/test/html/index.html ์— ์ €์žฅ๋œ ์ปค๋ฒ„๋ฆฌ์ง€ ์ ์ˆ˜ํ‘œ
๊ฐ ํด๋ž˜์Šค๋ฅผ ๋ˆŒ๋Ÿฌ๋ณด๋ฉด ์นœ์ ˆํ•˜๊ฒŒ ์–ด๋–ค ๋ถ€๋ถ„์ด ๋ฌธ์ œ์ธ์ง€๋„ ์•Œ๋ ค์ค€๋‹ค.

๋งŒ์•ฝ ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€์— ์ œ์™ธํ•˜๊ณ  ์‹ถ์€ ์ด๋ฆ„๊ทœ์น™์ด ์žˆ๋‹ค๋ฉด, ๊ทธ๋ฃจ๋น„ ๋ฌธ๋ฒ•์„ ์‚ฌ์šฉํ•ด์„œ exclude= [...]์— ์ ์šฉ์‹œ์ผœ์ฃผ์ž.

jacocoTestCoverageVerification {
def Qdomains = []
// ์ง์ ‘ํ•˜๋‚˜ํ•˜๋‚˜ ์ง€์ •ํ•ด์ค˜๋„ ๋˜๊ณ , ์ด๋Ÿฐ์‹์œผ๋กœ ํŒจํ„ด์„ ์ด์šฉํ•ด๋„ ๋œ๋‹ค.
for (qPattern in '*.QA'..'*.QZ') { // qPattern = '*.QA', '*.QB', ... '*.QZ'
Qdomains.add(qPattern + '*')
}
violationRules {
rule {
...
excludes = [] + Qdomains // ์ œ์™ธํ•  Qdomains ํŒจํ„ด ์ถ”๊ฐ€
}
}
}
}

 


# lombok์œผ๋กœ ์ƒ์„ฑ๋œ ์ฝ”๋“œ ์ œ์™ธํ•˜๊ธฐ

๊ทธ๋ƒฅ lombok ์„ค์ •ํ•˜๋‚˜ ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด ๋œ๋‹ค. ๊ฐ„๋‹จ

//build.gradle
lombok.config{
lombok.addLombokGeneratedAnnotation = true
}

 


# ์ „์ฒด ์„ค์ • ํ•œ๋ˆˆ์— ๋ณด๊ธฐ

์ฐธ๊ณ ๋กœ ๋ชจ๋“ˆ์„ ์—ฌ๋Ÿฌ๊ฐœ ์‚ฌ์šฉํ•  ๋•Œ, Jacoco๊ฐ€ ์‹คํ–‰๋„์ค‘ ์ปค๋ฒ„๋ฆฌ์ง€ ๊ธฐ์ค€๋ฏธ๋‹ฌ์ธ ๋ชจ๋“ˆ์„ ๋ฐœ๊ฒฌํ•˜๋ฉด ๋ฐ”๋กœ ์‹คํ–‰์„ ๋ฉˆ์ถ˜๋‹ค. ๊ทธ๋ž˜์„œ report๊ฐ€ ์ƒ์„ฑ๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๋„ ์žˆ๋Š”๋ฐ ๋งŒ์•ฝ ์‹คํŒจ์—ฌ๋ถ€์™€ ์ƒ๊ด€์—†์ด ๊ฐœ๋ณ„์ ์œผ๋กœ ์ „๋ถ€ ๋Œ๋ ค๋ณด๊ณ  ์‹ถ๋‹ค๋ฉด /gradlew test์— --continue๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ๋œ๋‹ค.

/*...*/
subprojects {
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'org.asciidoctor.convert'
apply plugin: 'jacoco' // ๋ฉ€ํ‹ฐ ๋ชจ๋“ˆ ์ ์šฉ
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
test {
useJUnitPlatform()
finalizedBy 'jacocoTestReport' // test ์ดํ›„ ์ปค๋ฒ„๋ฆฌ์ง€ ์‹คํ–‰
}
jacoco {
toolVersion = '0.8.6'
}
jacocoTestReport {
reports {
html.enabled true
csv.enabled true
xml.enabled false
}
def Qdomains = []
for (qPattern in '**/QA'..'**/QZ') { // qPattern = '*.QA', '*.QB', ... '*.QZ'
Qdomains.add(qPattern + '*')
}
afterEvaluate { // report ์ œ์™ธ๋Œ€์ƒ ์ง€์ •
classDirectories.setFrom(
files(classDirectories.files.collect {
fileTree(dir: it, excludes: [] + 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.00
}
limit {
counter = 'BRANCH'
value = 'COVEREDRATIO'
minimum = 0.00
}
excludes = [] + Qdomains // ์ปค๋ฒ„๋ฆฌ์ง€ ํ‰๊ฐ€ ์ œ์™ธ ๋Œ€์ƒ
}
}
}
}
/*...*/

 

๋‹ค์Œ ๊ธ€์—์„œ๋Š” Jacoco์˜ ์ฝ”๋“œ์ปค๋ฒ„๋ฆฌ์ง€ ๊ฒฐ๊ณผ๋ฅผ ์ด์šฉํ•ด์„œ ์ •์  ๋ถ„์„ CI ๋„๊ตฌ์ธ SonarQube์™€ ์—ฐ๋™ํ•ด๋ณด๋„๋ก ํ•˜๊ฒ ๋‹ค. ์ •์ ๋ถ„์„๋„๊ตฌ์™€ Jacoco์˜ ๋‹ค๋ฅธ์ ์€, Jacoco๋Š” ๋‹จ์ˆœํžˆ ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ๊ฒฐ๊ณผ๋ฅผ ์ด์šฉํ•ด์„œ ๋นŒ๋“œ ์„ฑ๊ณต/์‹คํŒจ์—ฌ๋ถ€๋ฅผ ์ธก์ •ํ•˜๋Š”๊ฒŒ ๋์ด๋ผ๋ฉด SonarQube๋Š” ์ฝ”๋“œ ํ’ˆ์งˆ๊ฒ€์‚ฌ๋ฅผ ํ†ตํ•ด ๋ฒ„๊ทธ, ์œ ๋‹›ํ…Œ์ŠคํŠธ, ์ฃผ์„, ๋ณต์žก๋„, ํ‘œ์ค€ ์ปจ๋ฒค์…˜(์ฝ”๋“œ Smell ์ธก์ •)๋“ฑ์„ ๋ถ„์„ํ•ด ์ทจ์•ฝ์ ๊ณผ ๋ณด๊ณ ์„œ๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

๋ธ”๋กœ๊ทธ์˜ ํ”„๋กœํ•„ ์‚ฌ์ง„

๋ธ”๋กœ๊ทธ์˜ ์ •๋ณด

JiwonDev

JiwonDev

ํ™œ๋™ํ•˜๊ธฐ