middlefitting

Jacoco를 활용하여 단위, 통합 테스트 분리하여 검증하기 본문

TestCode

Jacoco를 활용하여 단위, 통합 테스트 분리하여 검증하기

middlefitting 2024. 1. 3. 15:20

테스트 코드를 작성하다 보면 테스트 커버리지를 측정하고 싶은 경우가 생긴다. 측정을 넘어 테스트 커버리지에 제약을 걸어 새로 프로젝트의 커버리지를 높게 유지하고 싶다는 생각도 자연스럽게 생긴다. jacoco는 커버리지, 측정, 검증 도구로써 이런 고민을 해결해 줄 수 있다.

해당 글에서는 jacoco를 활용하여 단위 테스트와, 통합 테스트의 커버리지 측정 및 검증을 하는 방법을 알아보도록 한다. 환경은 스프링 부트, gradle 을 기준으로 한다.

 

우선 단위 테스트와 통합 테스트를 분리해서 측정하기 위해서는 해당 태스크가 분리되어 있어야 한다. 분리를 할 수 있는 방법은 아래의 글을 참고할 수 있다.

https://middlefitting.tistory.com/103

 

스프링에서 단위 테스트, 통합 테스트 쉽게 분리하기

스프링에서 테스트 코드를 작성하다 보면, 통합 테스트와 단위 테스트를 주로 작성하게 된다. 그런데 테스트 코드가 점점 많아지면서 관리에 어려움이 생긴다. 커버리지 측정에 있어 단위 테스

middlefitting.tistory.com

 

Jacoco 의존성 추가하기

 

먼저 Jacoco를 사용하기 위해서는 의존성을 추가해줘야 한다.

plugins {
   id 'jacoco'
}

jacoco {
	toolVersion = "0.8.8"
}

gralde의 해당 내용을 추가해주자. toolVersion에 경우는 사용하는 프로젝트에 맞게 추가해주면 된다. 의존성 추가 이후에는 Test 태스크 실행 이후 프로젝트의 루트 기준 ./build/jacoco/ 폴더에 *.exec 파일이 생기는 것을 확인할 수 있다. 기본적으로 태스크 명으로 생성되는데 파일명을 변경하고 싶으면

task unitTest(type: Test) {
   group = 'verification'
   description = 'Runs the unit tests.'
   useJUnitPlatform{
      includeTags 'UnitTest'
      excludeTags 'IntegrationTest'

   }

   jacoco {
      destinationFile = file("$buildDir/jacoco/test.exec")
   }

}

이렇게 destinationFile을 지정해주면 된다. 위의 경우에는 unitTest 태스크이지만 test.exec로 파일이 생겨나게 된다.

 

 

JacocoTestReport

 

JacocoTestReport는 앞서 말한 exec 파일을 기반으로 커버리지 보고서를 생성하는 태스크이다.

jacocoTestReport {

   reports {
      xml.enabled true
      html.enabled true
      csv.enabled false
   }

   afterEvaluate {
      classDirectories.setFrom(files(classDirectories.files.collect {
         fileTree(dir: it, exclude: jacocoExcludes)})
      )
   }
}

다음과 같은 형태로 작성되는데 xml, html, csv 파일중 생성하고자 하는 파일을 true로 지정해주면 된다. exclude의 경우에는 커버리지 측정에서 제외할 파일을 명시한다. 보통 dto, lombok, 외부 api 서비스에 경우는 단위, 통합 테스트에서 제외하는 경우가 많을 것이다. 필요에 따라 제외할 파일을 명시하자.

 

 

 

JacocoTestCoverageVerification
// 커버리지 검증 설정
jacocoTestCoverageVerification {

   violationRules {
      rule {
         enabled = true
         element = 'CLASS'

         //브랜치 커버리지
         limit {
            counter = 'BRANCH'
            value = 'COVEREDRATIO'
            minimum = 0.80
         }

         //메소드 커버리지
         limit {
            counter = 'METHOD'
            value = 'COVEREDRATIO'
            minimum = 0.80
         }

         //라인 커버리지
         limit {
            counter = 'LINE'
            value = 'COVEREDRATIO'
            minimum = 0.80
         }

         //검증에서 제외할 패키지, 클래스
         excludes = jacocoExcludes
      }
   }
}

JacocoCoverageVerification 태스크는 JacocoTestReport에서 생성된 커버리지 보고서를 기반으로 검증을 수행한다. 다양한 종류의 옵션을 설정할 수 있는데 위의 예시에서는 Class를 기반으로 브랜치 전체, 메소드, 라인 커버리지를 80%를 검증조건으로 설정한 것을 확인할 수 있다. 검증이 통과하면 해당 태스크는 성공하게되고 통과하지 못하면 실패하게 된다.

 

 

Test 타입의 태스크 만들기
//전체 테스트
test {
   description = 'Runs the total tests.'
   useJUnitPlatform()
}

//유닛 테스트
task unitTest(type: Test) {
   group = 'verification'
   description = 'Runs the unit tests.'
   useJUnitPlatform{
      includeTags 'UnitTest'
      excludeTags 'IntegrationTest'

   }

   jacoco {
      destinationFile = file("$buildDir/jacoco/test.exec")
   }

}

//통합 테스트
task integrationTest(type: Test) {
   group = 'verification'
   description = 'Runs the integration tests.'
   useJUnitPlatform{
      includeTags 'IntegrationTest'
      excludeTags 'UnitTest'
   }

   jacoco {
      destinationFile = file("$buildDir/jacoco/test.exec")
   }
}

그럼 이제는 Test 커스텀 태스크를 만들면 된다. 먼저 Test 타입의 태스크를 목적에 따라 정의한다. 위에서는 유닛 테스트와, 통합 테스트를 진행하는 태스크를 만들었다. 기본적으로 태스크 명으로 jacoco destincationFile이 생성되기 때문에, jacoco 태스크를 같이 활용하기 위해 파일명을 test 태스크에서 생기는 test.exec 파일로 고정한다. 

 

 

커스텀 태스크 만들기
//전체 테스트, 리포트 생성, 검증
task totalTestCoverage(type: Test) {
   group 'verification'
   description 'Runs the total tests with coverage'

   dependsOn(':test',
         ':jacocoTestReport',
         ':jacocoTestCoverageVerification')

   tasks['jacocoTestReport'].mustRunAfter(tasks['test'])
   tasks['jacocoTestCoverageVerification'].mustRunAfter(tasks['jacocoTestReport'])
}

//유닛 테스트, 리포트 생성, 검증
task unitTestCoverage(type: Test) {
   group 'verification'
   description 'Runs the unit tests with coverage'

   dependsOn(':unitTest',
         ':jacocoTestReport',
         ':jacocoTestCoverageVerification')

   tasks['jacocoTestReport'].mustRunAfter(tasks['unitTest'])
   tasks['jacocoTestCoverageVerification'].mustRunAfter(tasks['jacocoTestReport'])
}

//통합 테스트, 리포트 생성, 검증
task integrationTestCoverage(type: Test) {
   group 'verification'
   description 'Runs the integration tests with coverage'

   dependsOn(':integrationTest',
         ':jacocoTestReport',
         ':jacocoTestCoverageVerification')

   tasks['jacocoTestReport'].mustRunAfter(tasks['integrationTest'])
   tasks['jacocoTestCoverageVerification'].mustRunAfter(tasks['jacocoTestReport'])
}

그럼 이제 위의 태스크들을 조합해서 또 한번 커스텀 태스크를 만들면 된다. 해당 태스크들의 동작은 다음과 같은 형태로 이루어진다.

Test type의 태스크를 돌리고 생성된 test.exec 파일을 기반으로 jacocoTestReport 태스크로 커버리지 보고서를 만들고 해당 보고서를 jacocoTestCoverageVerification으로 검증한다. 태스크들의 실행순서가 중요하기 때문에 mustRunAfter을 활용하여 test -> report -> verification 순서로 실행되도록 제어해주자.

 

 

전체 내용

 

앞서 설명한 내용을 전체적으로 구성한 내용은 다음과 같다.

plugins {
	id 'jacoco'
}

//테스트 커버리지 측정도구
jacoco {
   toolVersion = "0.8.8"
}

// dto, 외부 연동 서비스는 테스트에서 제외
def    jacocoExcludes = [
   '*Application*',
   "**/config/*",
   "**/security/*",
   "**/dto/*",
   "**/aws/*",
   "*NotiMailSender*",
   '*SlackbotService*',
]

//커버리지 리포트 생성
jacocoTestReport {

   reports {
      xml.enabled true
      html.enabled true
      csv.enabled false
   }

   afterEvaluate {
      //dto 및 외부 연동 서비스는 테스트에서 제외
      classDirectories.setFrom(files(classDirectories.files.collect {
         fileTree(dir: it, exclude: jacocoExcludes)})
      )
   }
}

// 커버리지 검증 설정
jacocoTestCoverageVerification {

   violationRules {
      rule {
         enabled = true
         element = 'CLASS'

         //브랜치 커버리지
         limit {
            counter = 'BRANCH'
            value = 'COVEREDRATIO'
            minimum = 0.80
         }

         //메소드 커버리지
         limit {
            counter = 'METHOD'
            value = 'COVEREDRATIO'
            minimum = 0.80
         }

         //라인 커버리지
         limit {
            counter = 'LINE'
            value = 'COVEREDRATIO'
            minimum = 0.80
         }

         //검증에서 제외할 패키지, 클래스
         excludes = jacocoExcludes
      }
   }
}

//전체 테스트
test {
   description = 'Runs the total tests.'
   useJUnitPlatform()
}

//유닛 테스트
task unitTest(type: Test) {
   group = 'verification'
   description = 'Runs the unit tests.'
   useJUnitPlatform{
      includeTags 'UnitTest'
      excludeTags 'IntegrationTest'

   }

   jacoco {
      destinationFile = file("$buildDir/jacoco/test.exec")
   }

}

//통합 테스트
task integrationTest(type: Test) {
   group = 'verification'
   description = 'Runs the integration tests.'
   useJUnitPlatform{
      includeTags 'IntegrationTest'
      excludeTags 'UnitTest'
   }

   jacoco {
      destinationFile = file("$buildDir/jacoco/test.exec")
   }
}

//전체 테스트, 리포트 생성, 검증
task totalTestCoverage(type: Test) {
   group 'verification'
   description 'Runs the total tests with coverage'

   dependsOn(':test',
         ':jacocoTestReport',
         ':jacocoTestCoverageVerification')

   tasks['jacocoTestReport'].mustRunAfter(tasks['test'])
   tasks['jacocoTestCoverageVerification'].mustRunAfter(tasks['jacocoTestReport'])
}

//유닛 테스트, 리포트 생성, 검증
task unitTestCoverage(type: Test) {
   group 'verification'
   description 'Runs the unit tests with coverage'

   dependsOn(':unitTest',
         ':jacocoTestReport',
         ':jacocoTestCoverageVerification')

   tasks['jacocoTestReport'].mustRunAfter(tasks['unitTest'])
   tasks['jacocoTestCoverageVerification'].mustRunAfter(tasks['jacocoTestReport'])
}

//통합 테스트, 리포트 생성, 검증
task integrationTestCoverage(type: Test) {
   group 'verification'
   description 'Runs the integration tests with coverage'

   dependsOn(':integrationTest',
         ':jacocoTestReport',
         ':jacocoTestCoverageVerification')

   tasks['jacocoTestReport'].mustRunAfter(tasks['integrationTest'])
   tasks['jacocoTestCoverageVerification'].mustRunAfter(tasks['jacocoTestReport'])
}

 

jacoco 관련해 더 자세한 내용은 아래 공식문서를 참고할 수 있다.

https://docs.gradle.org/current/userguide/jacoco_plugin.html

 

The JaCoCo Plugin

The JaCoCo plugin adds the following dependency configurations: Table 2. JaCoCo plugin - dependency configurations Name Meaning jacocoAnt The JaCoCo Ant library used for running the JacocoReport and JacocoCoverageVerification tasks. jacocoAgent The JaCoCo

docs.gradle.org