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

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