2024. 4. 29. 21:07ㆍ스프링 (Spring)/스프링 팁 (Spring Tip)
jacoco는 코드 커버리지를 보여주는 라이브러리입니다.
물론 커버리지가 높다고해서 100% 안정성을 보장해주지 않지만, 커버리지가 낮으면 안정성도 낮은 건 맞습니다.
테스트를 가시적으로 보지 않으면, 놓치는 것도 있으니 이를 보완할 겸 사용하는 걸 추천합니다.
jacoco Plugin
기본적으로 The JaCoCo Plugin - Gradle 문서를 참고하면 됩니다.
먼저 build.gradle.kts 을 열어서 jacoco 플러그인을 추가합니다.
plugins {
...
jacoco
}
그리고 gradle reload 를 수행합니다.
jacoco Settings
여전히 build.gradle.kts 에서 작성하시면 됩니다.
Test
Spring 3.* 버전을 사용하므로 JUnit에 대해 기본 설정이 되어 있긴하지만,
없을 때를 대비해 useJUnitPlatform() 을 추가한 설정 그대로 가져왔습니다.
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.test {
finalizedBy(tasks.jacocoTestReport)
}
이렇게 만들면 test 태스크가 끝나고 jacocoTestReport 태스크가 이어서 수행됩니다.
JacocoReport
스캔 대상에서 특정 디렉터리나 파일을 제외하려면 아래와 같이 exclude 함수를 이용합니다.
tasks.withType<JacocoReport> {
afterEvaluate {
classDirectories.setFrom(classDirectories.files.map {
fileTree(it) {
exclude("**/infra/**")
}
})
}
}
여담으로 제가 사용한 설정은 다음과 같습니다.
tasks.withType<JacocoReport> {
afterEvaluate {
classDirectories.setFrom(classDirectories.files.map {
fileTree(it) {
exclude(
"**/*Application*",
"**/infra/configuration/**/*Configuration.class",
"**/infra/dao/**",
"**/app/controller/**/*Controller.class",
"**/app/controller/**/*Dto.class",
"**/app/controller/**/*Dto?Companion*",
"**/domain/service/**/*Vo.class"
)
}
})
}
}
다음으로 jacocoTaskReport 태스크를 설정합니다.
tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
xml.required = false
csv.required = false
html.outputLocation = layout.buildDirectory.dir("jacocoHtml")
}
finalizedBy(tasks.jacocoTestCoverageVerification)
}
xml, csv는 무시하고 html 만 생성하도록 설정했습니다.
이렇게 만들면 jacocoHtml 디렉터리에 리포트(index.html)가 생성됩니다.
필터 예시 참고
패턴 사용법 참고
JacocoTestCoverageVerification
코드 커버리지가 정한 비율을 넘는 지 추가로 검사할 수 있습니다.
tasks.jacocoTestCoverageVerification {
dependsOn(tasks.jacocoTestReport)
violationRules {
rule {
limit {
minimum = "0.8".toBigDecimal()
}
}
}
}
이렇게 설정하면 커버리지가 80%를 넘어야 테스트가 통과합니다.
2024. 05. 02 : 여기도 exclude를 써야 했네요.
도 모르고 커버리지 통과가 왜 안 되나 했네요...
JacocoReport와 공용으로 사용할 것이기 때문에 변수로 만들어두고
val coverageExcludePaths = listOf(
"**/*Application*",
"**/infra/configuration/**/*Configuration.class",
"**/infra/dao/**",
"**/app/controller/**/*Controller.class",
"**/app/controller/**/*Dto.class",
"**/app/controller/**/*Dto?Companion*",
"**/domain/service/**/*Vo.class",
"**/domain/base/**/*Vo.class"
)
아래처럼 JacocoCoverageVerification에 afterEvaluate를 추가했습니다.
tasks.withType<JacocoCoverageVerification> {
afterEvaluate {
classDirectories.setFrom(classDirectories.files.map {
fileTree(it) {
exclude(coverageExcludePaths)
}
})
}
}
jacoco Report
리포트를 보려면 index.html 를 실행하면 됩니다.
아직 테스트 코드를 안 짜서 개판이네요.
해당 결과를 예쁘게 보여주는 뷰어로는 sonarqube가 있습니다. 실무에서 유용하게 잘 써온 건데
지금 막상 찾아보니 개인이 쓰려면 연간 20만원 정도 줘야하는군요...
마땅한 뷰어 프로젝트도 없는 것 같긴한데, 많이 아쉽네요. 시간이 남으면 ui나 개발해봐야 겠네요.
Miscellaneous Efforts
적용하면서 잡다하게 알아낸 것들을 소개해보겠습니다.
jacoco.excludes
처음에는 스캔 대상을 제외하는 걸로 이해했는데, 사실 아니었고
tasks.test {
finalizedBy(tasks.jacocoTestReport)
jacoco {
excludes += "**/*ImplTest.class"
}
}
와 같이 test 디렉터리에 위치한 테스트 코드를 제외하기 위함이었습니다.
이것도 모르고 괜히 왜 안되나 찾아보았네요...
jacocoTestReport.doFirst, doLast
tasks.withType<JacocoReport>
스캔 제외 대상을 위 블록에 작성했었는데,
태스크에서 설정하는 것과 withType에서 설정하는 것에 대해 차이를 몰랐습니다.
따라서 아래처럼 doFirst, doLast로 스캔 대상에서 제외하는 로직을 넣었으나
tasks.jacocoTestReport {
...
doFirst { // 또는 doLast
classDirectories.setFrom(classDirectories.files.map {
fileTree(it) {
exclude("**/infra/**")
}
})
}
}
Execution failed for task ':jacocoTestReport'.
> The value for this file collection is final and cannot be changed.
와 같이 immutable 하기 때문에 classDirectories를 건드릴 수 없었네요.
조금 더 파보고 싶어서 찾아가봤는데
public void setFrom(Iterable<?> path) {
this.assertMutable();
this.value = this.value.setFrom(...);
}
에서 assertMutable()을 따라가고,
private void assertMutable() {
if (this.state == DefaultConfigurableFileCollection.State.Final && this.disallowChanges) {
throw new IllegalStateException("The value for " + this.displayNameForThisCollection() + " is final and cannot be changed.");
} else if (this.disallowChanges) {
throw new IllegalStateException("The value for " + this.displayNameForThisCollection() + " cannot be changed.");
} else if (this.state == DefaultConfigurableFileCollection.State.Final) {
throw new IllegalStateException("The value for " + this.displayNameForThisCollection() + " is final and cannot be changed.");
}
}
public abstract class JacocoReportBase extends JacocoBase {
private final ConfigurableFileCollection executionData = this.getProject().files(new Object[0]);
private final ConfigurableFileCollection sourceDirectories = this.getProject().files(new Object[0]);
private final ConfigurableFileCollection classDirectories = this.getProject().files(new Object[0]);
private final ConfigurableFileCollection additionalClassDirs = this.getProject().files(new Object[0]);
private final ConfigurableFileCollection additionalSourceDirs = this.getProject().files(new Object[0]);
...
까지 와서 추정할 수 있는 건, 결국 doFirst, doLast 시점에는 해당 컬렉션의 상태가 Final로 바뀐다는 것을 짐작할 수 있었습니다.
그렇다면 afterEvaluate 까지는 mutable 하다는 것인데, 이는 Plugin Lifecycle 과도 관계가 있겠네요.
이는 나중에 조금 더 공부해보면 좋을 것 같습니다.
'스프링 (Spring) > 스프링 팁 (Spring Tip)' 카테고리의 다른 글
Kotlin 플러그인 All-open 파헤치기 (feat, JPA Fetch) (0) | 2024.05.18 |
---|---|
테스트 시 발생하는 위험 메시지 무시해보기 (0) | 2024.05.01 |
Kotlin 답게 간단하게 로깅해보기 (0) | 2024.04.30 |
Mockito를 이용해 JpaRepository 테스트하기 (0) | 2024.04.30 |
Spring에서 Gmail 보내기 (1) | 2024.04.29 |