Jacoco ๋?
Java Code Coverage ์ ์ค์๋ง๋ก, ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง ๋ถ์์ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค.
โ ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง๋?
์ํํธ์จ์ด์ ํ ์คํธ ์ผ์ด์ค๊ฐ ์ผ๋ง๋ ์ถฉ์กฑ๋์๋์ง๋ฅผ ๋ํ๋ด๋ ์งํ ์ค ํ๋์ด๋ค.
1. ๋ผ์ธ ์ปค๋ฒ๋ฆฌ์ง : ์ฝ๋ ํ ์ค์ด ํ ๋ฒ ์ด์ ์คํ๋๋ค๋ฉด ์ถฉ์กฑ๋๋ค.
2. ๊ฒฐ์ ์ปค๋ฒ๋ฆฌ์ง : ๋ธ๋์น ์ปค๋ฒ๋ฆฌ์ง๋ผ๊ณ ๋ ๋ถ๋ฆฐ๋ค. ๋ชจ๋ ์กฐ๊ฑด์์ ๋ด๋ถ ์กฐ๊ฑด์ด true/ false ๋ฅผ ๊ฐ์ง๊ฒ ๋๋ฉด ์ถฉ์กฑํ๋ค.
3. ์กฐ๊ฑด ์ปค๋ฒ๋ฆฌ์ง : if ์กฐ๊ฑด๋ฌธ ์์ ๊ฐ๋ณ ์กฐ๊ฑด์์ด false ์ธ๊ฒฝ์ฐ์ true์ธ ๊ฒฝ์ฐ ๋ชจ๋ ์คํ๋์์ ๋ ์ถฉ์กฑํ๋ค. ๊ฒฐ์ ์ปค๋ฒ๋ฆฌ์ง๋ณด๋ค ๋ detailํ ์ปค๋ฒ๋ฆฌ์ง์ด๋ค.
์ ์ฉํด๋ณด์
์ ์ฉ ์ build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.2'
id 'io.spring.dependency-management' version '1.1.2'
}
group = 'com'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
plugin ์ถ๊ฐ
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.2'
id 'io.spring.dependency-management' version '1.1.2'
// ์ถ๊ฐ ๋ ๋ถ๋ถ
id 'jacoco'
}
jacoco ํ๋ฌ๊ทธ์ธ์ ์ถ๊ฐํ๋ค.
jacoco ๋ฒ์ ๊ณผ ๋ถ์ ๋ฆฌํฌํธ ๊ฒฝ๋ก ์ค์
jacoco {
toolVersion = "0.8.10"
reportsDirectory = layout.buildDirectory.dir('jacocoReport')
}
์์ ๊ฐ์ด ๋ถ์ ๋ฆฌํฌํธ๊ฐ ์์ฑ๋๋ ๊ฒฝ๋ก๋ฅผ ์ง์ ํ๋ฉด, build > jacocoReport ๋ผ๋ ๋๋ ํ ๋ฆฌ๊ฐ ์์ฑ๋๊ณ
ํด๋น ๋๋ ํ ๋ฆฌ ์์ ๋ฆฌํฌํธ๊ฐ ์์ฑ๋๋ค.
test task ์คํ ํ jacocoTestReport ์คํ ๋๊ฒ๋ ์ค์
// ๊ธฐ์กด์ ์๋ ์๋ ์ฝ๋ ์ ๊ฑฐ
//
// tasks.named('test') {
// useJUnitPlatform()
// }
test {
finalizedBy jacocoTestReport
useJUnitPlatform()
}
์์ ๊ฐ์ด ์์ ํ๋ค.
finalizedBy jacocoTestReport
๋ test task ์คํ ํ, jacocoTestReport
task ๋ฅผ ์คํํ๊ฒ๋ ํ๋ ๊ตฌ๋ฌธ์ด๋ค.
ํ ์คํธ๋ฅผ ์คํํ ๋๋ง๋ค jacoco ๋ฆฌํฌํธ๊ฐ ์์ฑ๋ ๊ฒ์ด๋ค. (์ ์ธํ๊ณ ์ถ์ ์ฌ๋์ ์ ์ธํด๋ ๋๋ค. ๋์ ๋ถ์ ๋ฆฌํฌํธ๋ฅผ ์์ฑํ๋ ค๋ฉด ๋ฐ๋ก ์คํํด์ผํ๋ค.)
jacocoTestReport task ์ค์
jacocoTestReport {
dependsOn test
reports {
xml.required = false
csv.required = false
html.required = true
}
afterEvaluate {
classDirectories.setFrom(
files(classDirectories.files.collect {
fileTree(dir: it, excludes: [
'**/domain/**',
'**/global/**',
'**/*Application*'
])
})
)
}
finalizedBy 'jacocoTestCoverageVerification'
}
dependsOn test
๋ก test
task ์คํ ๋ค ์คํ๋๋๋ก ๋ช
์ํ๋ค.
reports ๋ html ํ์ผ๋ก๋ง ํ์ธํ ๊ฒ์ด๋ฏ๋ก html.required
๋ง true
๋ก ํ๋ค.
๊ทธ๋ฆฌ๊ณ afterEvaluate{ ... }
๊ตฌ๋ฌธ์ excludes : [ ... ]
์์ ๋ถ์ ๋ฆฌํฌํธ์์ ์ ์ธํ ํด๋์ค๋ฅผ ์ ํํ๋ค.
๋๋ service ์ฝ๋์ controller ํด๋์ค๋ง ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ์ ๊ฒํ๊ธฐ ์ํด, ํ๋ก์ ํธ ๋ด์ domain · global ๋๋ ํ ๋ฆฌ ๊ทธ๋ฆฌ๊ณ Application ํด๋์ค ํ์ผ์ ์ ์ธํ๋ค.
๊ทธ๋ฆฌ๊ณ finalizedBy 'jacocoTestCoverageVerification'
๋ฅผ ํตํด ๋ค์ ์งํํ task๋ฅผ ์ค์ ํ๋ค.
jacocoTestCoverageVerification task ์ค์
jacocoTestCoverageVerification {
violationRules {
rule {
enabled = true;
element = 'CLASS'
// ๋ผ์ธ ์ปค๋ฒ๋ฆฌ์ง ์ ํ์ 90%๋ก ์ค์
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.90
}
excludes = [
'**.*Application',
'**.domain.**',
'**.global.**'
]
}
}
}
์ ๋ถ๋ถ์, ์ปค๋ฒ๋ฆฌ์ง ๊ธฐ์ค์ ์ค์ ํ ์ ์๋ค.
ํด๋์ค ๋จ์๋ก ๋ผ์ธ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ์ต์ 90% ๋ง์กฑ์์ผ์ผ ํ๋ ์ต์ ์ ์ค์ ํ๋ค.
๋ํ, ์ปค๋ฒ๋ฆฌ์ง ๊ฒ์ฆ์์ ์ ์ธํ ๊ฒฝ๋ก๋ ์ถ๊ฐํ๋ค.
jacocoTestReport
์์ ์ฌ์ฉํ๋ ํํ์๊ณผ๋ ์ฝ๊ฐ ์ฐจ์ด๊ฐ ์์ผ๋ ์ฃผ์ํด์ผํ๋ค.
์์์ ์ค์ ํ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ๋ง์กฑํ์ง ๋ชฉํ๋ฉด ๋น๋ ๊ณผ์ ์์ ์๋ฌ๊ฐ ๋ฐ์ํ๋๋ก ๋์ด ์์ด ํ ์คํธ ์ฝ๋๋ฅผ ์ง๊ฒ๊ธ ๊ฐ์กฐํ ์ ์๊ณ ์์ ํ ์ฝ๋๋ฅผ ์งํฅํ ์ ์๊ฒ ๋๋ค.
์ ์ฉ ํ build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.2'
id 'io.spring.dependency-management' version '1.1.2'
id 'jacoco'
}
group = 'com'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
finalizedBy jacocoTestReport
useJUnitPlatform()
}
jacoco {
toolVersion = "0.8.10"
reportsDirectory = layout.buildDirectory.dir('jacocoReport')
}
jacocoTestReport {
dependsOn test
reports {
xml.required = false
csv.required = false
html.required = true
}
afterEvaluate {
classDirectories.setFrom(
files(classDirectories.files.collect {
fileTree(dir: it, excludes: [
'**/domain/**',
'**/global/**',
'**/*Application*'
])
})
)
}
finalizedBy 'jacocoTestCoverageVerification'
}
jacocoTestCoverageVerification {
violationRules {
rule {
enabled = true;
element = 'CLASS'
// ๋ผ์ธ ์ปค๋ฒ๋ฆฌ์ง ์ ํ์ 90%๋ก ์ค์
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.90
}
excludes = [
'**.*Application',
'**.domain.**',
'**.global.**'
]
}
}
}
Jacoco Report ํ์ธ
์ด์ ๋ค์ ๋น๋๋ฅผ ํ๊ณ , ํฐ๋ฏธ๋์
./gradlew clean test
์ ๋ช ๋ น์ด๋ฅผ ์ ๋ ฅํ๊ฑฐ๋
intellij ์์ test task๋ฅผ ํด๋ฆญํ๋ฉด ๋ถ์ ๋ฆฌํฌํธ๊ฐ ์์ฑ๋๋ค.
์ค์ ํ๋ ๊ฒฝ๋ก์ index.html
์ด๋ผ๋ ๋ฆฌํฌํธ๊ฐ ์์ฑ๋์๊ณ ์ด์ด๋ณด๋ฉด
๋ผ์ธ ์ปค๋ฒ๋ฆฌ์ง์ ๋ธ๋์น ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ๋์ผ๋ก ํ์ธํ ์ ์๋ค.
ํด๋ฆญํด์ ์์ธ ํ๋ฉด์ ๋ค์ด๊ฐ๋ฉด, ์์ ๊ฐ์ด ๋ผ์ธ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ํ์ธํ ์ ์๋ค.
ํธ๋ฌ๋ธ ์ํ
๋ง์ฝ, test ๋ฆฌํฌํธ๊ฐ ์์ฑ์ด ์ ์๋๊ฑฐ๋ ๋ฆฌํฌํธ ๋ด์ ํ๊ธ์ด ๊นจ์ง๊ฑฐ๋ ํ๋ ๊ฒฝ์ฐ๋
์ค์ ํ๋ก์ ํธ๊ฐ ์กด์ฌํ๋ ๊ฒฝ๋ก์ ํ๊ธ๋ก ๋ ๋๋ ํ ๋ฆฌ๊ฐ ํฌํจ๋์ด์๋์ง ํ์ธํด๋ณด๋ ๊ฒ์ด ์ข๋ค.
๋๋ ๋ถ์ ๊ฒฐ๊ณผ ๋ฆฌํฌํธ์ ์ฃผ์์ผ๋ก ์จ๋์ ํ๊ธ์ด ๊ณ์ ๊นจ์ ธ์ ํ์ฐธ์ ํด๊ฒฐํด๋ณด๋ ค๋ค๊ฐ
์ค์ ์ปดํจํฐ์ ์ ์ฅ๋์ด ์๋ ํ๋ก์ ํธ์ ๊ฒฝ๋ก๊ฐ ํ๊ธ๋ก ๋ ๋๋ ํ ๋ฆฌ ๋ด์ ์์๊ณ ,
์ด๋ฅผ ํด๊ฒฐํ๋ ํ๊ธ์ด ์ ๋์ค๊ฒ ๋์๋ค.