eelseungmin

코드 커버리지 도입기

by eelseungmin

들어가며

최근 프로젝트를 개발하면서 API를 구현한 뒤 기능을 검증하기 위해 열심히 테스트를 작성하고 있다.

약간 기계적으로 작성하고 있었다는 생각도 들어서, 코드 커버리지에 대한 이야기를 하기 전에 테스트를 작성하는 이유부터 짚고 넘어가고자 한다.

 

테스트를 작성하는 이유

먼저 전통적으로 Postman 등을 이용해 직접 API를 호출하고 요청을 일일이 만들어가면서 테스트한다고 가정해보자.

제품, 그러니까 프로젝트의 규모가 작을 때는 사람이 직접 값을 집어넣어 가면서 테스트를 진행해도 크게 문제가 없을 것이다.

하지만 사용자가 많아지고 프로젝트의 규모가 거대해진다면...?

Practical Testing: 실용적인 테스트 가이드

테스트가 필요한 케이스 또한 굉장히 많아질 것이고, 일일이 요청을 날리는데 필요한 시간과 노력도 결코 무시할 수 없다. 그 말은 즉슨 유지보수가 어려워진다는 것이고 그에 따라 제품의 신뢰도 또한 낮아질 수밖에 없다.

 

Practical Testing: 실용적인 테스트 가이드

 

이러한 상황에서 테스트 코드 작성을 도입하게 되면

1. 코드 작성 이후 곧바로 테스트를 돌려봄으로써 빠른 피드백을 기대할 수 있다. 이는 로직이나 테스트 코드에 어떠한 오류가 존재하더라도 수정을 빠르게 할 수 있다는 의미도 된다.

2. 테스트 과정 자체를 코드 작성을 통해 자동화시킬 수 있기 때문에 매번 똑같은 기능을 테스트하는 데 비용을 소모할 필요가 없어진다.

 

이제는 테스트를 작성하지 않고 직접 테스트를 하는 게 더 부자연스러울 정도이다.

물론 Postman을 사용해야 하는 경우가 종종 있긴 하다...

 

내가 작성한 테스트의 객관적 기준이 필요하다...!

In software engineering, code coverage is a percentage measure of the degree to which the source code of a program is executed when a particular test suite is run. A program with high test coverage has more of its source code executed during testing, which suggests it has a lower chance of containing undetected software bugscompared to a program with low test coverage.

위키피디아 - Code Coverage

 

코드 커버리지란 작성된 테스트에서 실행되는 코드 블록을 식별하여, 그 빈도를 %로 나타낸 것이다. 따라서 코드 커버리지를 보면 해당 테스트가 얼마나 프로젝트의 코드를 많이 커버하는지 한눈에 알 수 있다.

어느 정도 테스트를 작성해야 하는지 테스트 관련 객관적 지표가 없어 조금 답답해하던 차에 코드 커버리지를 도입함으로써 이를 해소하고자 했다. 

 

JaCoCo

Java 코드의 커버리지를 체크하기 위한 도구로서 JaCoCo를 많이들 언급하길래 지금 개발 중인 프로젝트에 도입해 보았다.

 

build.gradle(의존성 설정)

build.gradle에 다음과 같이 설정하자. 내가 사용한 Spring Boot는 3.2.0이고 구글링하면 찾을 수 있는 여러 레퍼런스 자료들과는 Gradle 관련 문법이 조금씩 다를 수 있다.

plugins {
    // 생략
    id 'jacoco'
}

test {
    useJUnitPlatform()

    finalizedBy 'jacocoTestReport' // 해당 task 이후 반드시 실행할 task를 명시한다.
}

jacoco {
    toolVersion = '0.8.11' // 작성 당시 최신 버전으로 설정해주었다.
}

jacocoTestReport { // 생성된 커버리지 문서를 사람이 인식하기 쉬운 형태의 파일로 변환하는 task이다.
    reports {
        html.required.set(true) // html 형태만 사용할 것이기에 html만 true로 설정하였다.
        xml.required.set(false)
        csv.required.set(false)
    }

    afterEvaluate {
        classDirectories.setFrom(
                files(classDirectories.files.collect {
                    fileTree(dir: it, excludes: [ // 해당 영역에 Ant Style Pattern을 이용해 특정 클래스나 패키지를 문서에서 제외할 수 있다.
                            "**/config/**",
                            "**/*Application*",
                            "**/request/**",
                            "**/response/**",
                            "**/dto/**",
                            "**/exception/**",
                            "**/filter/**",
                            "**/*Filter*",
                            "**/converter/**",
                            "**/HealthCheckController.class",
                            "**/oauth2/**"
                    ])
                })
        )
    }
}

jacocoTestReport와 jacocoTestCoverageVerifiaction 두 가지 task가 있는데, 전자는 커버리지 문서의 설정을 바꾸기 위한 것이고 후자는 본인이 설정한 커버리지를 넘기지 못한 경우 빌드가 실패하도록 커버리지 기준을 설정할 수 있다.

jacocoTestCoverageVerifiaction의 경우 현재 프로젝트에서 사용하지 않기 때문에 지금 설명하지는 않고 추후 사용하게 되면 설명을 추가해보려고 한다.

 

문서 생성하기

터미널에 다음과 같이 입력한다.

./gradlew clean
./gradlew test

 

그러면 다음과 같이 default 경로인 ./build/reports/jacoco/test/html 경로에 index.html이라는 커버리지 문서가 생성된다.

 

처음에 클래스나 패키지를 아무것도 제외하지 않으면 아래처럼 모든 패키지가 다 표시된다.

 

원하지 않는 패키지, 클래스 제외하기

jacocoTestReport {
    reports {
        html.required.set(true)
        xml.required.set(false)
        csv.required.set(false)
    }

    afterEvaluate {
        classDirectories.setFrom(
                files(classDirectories.files.collect {
                    fileTree(dir: it, excludes: [
                            "**/config/**",
                            "**/*Application*",
                            "**/request/**",
                            "**/response/**",
                            "**/dto/**",
                            "**/exception/**",
                            "**/filter/**",
                            "**/*Filter*",
                            "**/converter/**",
                            "**/HealthCheckController.class",
                            "**/oauth2/**"
                    ])
                })
        )
    }
}

여기서 굳이 커버리지를 나타낼 필요가 없는 DTO 클래스 등을 build.gradle에서 제외하면 다음과 같이 변한다.

 

Lombok 관련 클래스 제외하기

여기서 Lombok을 사용했을 경우 @Builder나 @Getter로 인해 생성된 메서드까지 전부 커버리지 체크에 포함이 되어버린다. 다음과 같이 설정하자.

lombok.addLombokGeneratedAnnotation = true

프로젝트 루트 경로(build.gradle이 위치한 경로)에 lombok.config 파일을 생성한 뒤 위와 같이 내용을 입력하고 다시 실행해보면 깔끔하게 제외할 수 있다.

 

해당 옵션을 true로 설정하면 Lombok과 관련된 모든 클래스, 메서드, 필드에 lombok.@Generated라는 어노테이션이 추가되도록 하여 JaCoCo가 이를 인식하지 못하도록 하는 원리라고 한다.

 

현재 프로젝트에서 JaCoCo는 내가 느낀 불편함을 해소하기 위해 도입하긴 했지만, 팀원한테 테스트 코드 작성이나 커버리지 체크를 강요할 수는 없어 커버리지 기준 체크까지는 도입하지 않았다. 만약 팀원끼리의 합의가 되어 실무에서 이를 적용한다면 견고한 테스트 작성에 큰 도움이 될 것이라고 생각한다.

 

마지막으로 알아두면 좋을 것 같은 내용에 대해 인용문을 추가하면서 이번 글을 마무리하겠다.

 

우리가 꼭 기억해야 할 점은 코드 커버리지 100% 가 무결점 소프트웨어를 의미하진 않는다는 점입니다. 코드 커버리지가 100%가 되었다고 하더라도 아래의 경우에서 문제점이 발생할 수 있기 때문입니다.
1. 요구사항에 있는 기능이 누락된 경우
2. 구현에 오류가 있는 경우
3. 테스트 케이스가 누락된 경우
따라서 단순히 코드 커버리지 숫자를 높이기 위해 테스트 케이스를 생성하는 일은 지양해야 하며, 요구사항 및 구현 코드에 대한 이해를 바탕으로 올바른 테스트 케이스를 작성해야 합니다.
구글에서 제시하고 있는 코드 커버리지 수준은 60%를 “적절한 수준”, 75%를 “칭찬받을 만한 수준”, 90%를 “모범적인 수준”으로 가이드하고 있습니다.

인용 출처:https://medium.com/uplusdevu/code-coverage-c252e271df60

 

참고

https://inf.run/YLRXA

 

Practical Testing: 실용적인 테스트 가이드 강의 - 인프런

이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을 위한 강

www.inflearn.com

https://techblog.woowahan.com/2661/

 

Gradle 프로젝트에 JaCoCo 설정하기 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요. 상품시스템팀에서 서버 개발(..새발)을 하고 있는 연철입니다. 프로젝트 세팅 중에 찾아보고 삽질했던 내용들이 도움이 될까 하여 남깁니다. JaCoCo는 Java 코드의 커버리지

techblog.woowahan.com

https://hudi.blog/dallog-jacoco/

 

달록의 Jacoco 적용기 (feat. Gradle)

이 글은 우아한테크코스 4기 달록팀의 기술 블로그에 게시된 글 입니다. 코드 커버리지란? 코드 커버리지란, 테스트 코드가 프로덕션 코드를 얼마나 실행했는지를 백분율로 나타내는 지표입니

hudi.blog

https://medium.com/uplusdevu/code-coverage-c252e271df60

 

Code Coverage

Code Coverage 누구냐? 넌!

medium.com

https://www.baeldung.com/jacoco-report-exclude

블로그의 정보

eel.log

eelseungmin

활동하기