계기가 된 건 토스뱅크 이응준님의 테스트 커버리지 100% 영상을 보고 난 후이다. 테스트코드를 작성하는 것이 적은 공수가 들어가는 것이 아니라는걸 알고 있었기 때문에 이걸 도전한다는 것에 큰 영감을 받았고, 크기가 커지면서 생기는 이슈를 해결하는 과정 또한 흥미로웠다.
얼마만큼의 코드를 자동화한 단위 테스트로 계산해야 할까? 대답할 필요조차 없다. 모조리 다 해야 한다. 모.조.리! 100% 테스트 커버리지를 권장하냐고? 권장이 아니라 강력히 요구한다. 작성한 코드는 한 줄도 빠짐없이 전부 테스트해야 한다. 군말은 필요 없다. ― 클린 코더 (로버트 마틴 저)
테스트코드의 중요성은 원래 알고있었다. 리팩토링 할 때 심리적 안정감을 느낄 수 있고, 클래스 간의 결합 관계에 대해 더욱 파악이 쉬워지고, 그로 인해 객체지향적 설계에 도움이 된다. 하지만 프로젝트를 진행하며 아무래도 기능 개발이 아닌 추가적인 작업이라고 무의식적으로 생각을 했고, 기능 구현과 테스트코드 작성을 별개의 것으로 두고 진행했다.
먼저 정적 분석이란 무엇인가에 대해 알아볼 필요가 있다. 우리는 테스트 패키지에 코드를 작성하고, 테스트 어노테이션이 붙은 부분을 실행해서 결과를 확인한다. 즉, 런타임 환경에서 수행된다. 하지만 이런 동적 테스트만으로는 한계점이 있다. 개발자는 모든 테스트케이스를 예상할 수 없다. 사용자가 해당 포맷이 아닌 입력을 할 수 있고, 문자열에 뜬금 없이 정수값이 들어온다거나, 양수로 받아 진행되는 로직에 음수를 입력할 수 있다. 이런 모든 상황을 예측할 수 없다는 얘기다. 또한 코드 컨벤션이나, 사용되지 않는 코드 또한 동적 테스트만으로는 알 수 없는 문제이다.
이런 한계점 때문에 우리는 정적 분석을 도입한다. 테스트를 실행하기 전에 분석이 가능하다. 대표적으로 SonarQube, Jacoco가 있고, Jacoco에서는 코드 커버리지 리포트를 제공하므로 도입하기로 결정했다.
build.gradle에 플러그인을 추가하고, 테스트를 실행하면 다음과 같은 리포트가 생성된다.

다음은 코드 커버리지를 살펴보자

패키지를 클릭하면 테스트에 사용되지 않는 코드를 확인할 수 있다.

테스트 커버리지를 높이는 건 생각보다 쉽지 않았다. 먼저 사용되지 않는 메소드나 어노테이션을 제거했는데 equals()는 사용하지만 hashcode()는 사용되지 않거나 테스트코드로 작성하지는 않았지만 스프링 컨테이너에서 사용되는 부분들이 있었다. 이 과정에서 equals()와 hashcode()가 같이 존재해야 하는 이유를 공부하기도 하고 이 메소드가 필요한지 필요하지 않은지 고민하는 데에 시간을 썼다. 또한 예외처리가 테스트되지 않은 경우도 있었는데 따로 커스텀 예외를 생성해 별도의 패키지에서 관리하는게 낫다고 생각해 todo리스트에 추가했다. 확정성을 위해 단순히 뼈대만 작성한 코드들도 테스트코드를 작성하지 않았는데, 이 부분은 그대로 두기로 결정했다.
확실하게 느낀 건 어플리케이션에 대한 이해도이다. 인증이나 설정 등 프레임워크에 구현을 위임한 기능들은 테스트코드를 작성하는 게 쉽지 않다. 직접 어노테이션을 까서 들어가보기도 하면서 어떻게 파라미터를 주입받고 어떤 원리로 동작하는지에 대해 알고 있어야 주도적으로 테스트코드를 작성할 수 있다. 사실 이런 계기를 갖는다는 것이 당연하면서도 쉽지 않은데, 테스트코드를 작성하면서 알게 되어 뿌듯했다.

그 다음 테스트 커버리지에 크게 의미가 없는 dto 패키지를 제외하고 커버리지가 75%가 되지 않으면 빌드가 되지 않도록 설정했다. 75%로 정한 이유는 매번 기능 확장을 할 때 테스트를 작성하면 훨씬 많은 시간이 들어간다는 점, 그럼에도 반드시 TDD와 커버리지 100%를 지향해야 한다는 점을 고려해여 75%가 적당하다고 생각했다.
jacocoTestCoverageVerification {
violationRules {
rule {
enabled = true
element = 'CLASS'
limit {
counter = 'CLASS'
value = 'COVEREDRATIO'
minimum = 0.75
}
excludes = [
'*.*Application',
'*.dto.*'
]
}
}
}
물론 높은 커버리지가 테스트를 보장하지는 않는다. 커버리지가 100%여도 에러가 날 수 있다. 그래도 이번 작업을 진행하며 바뀐 점은 기능 구현과 테스트코드 작성은 별개의 것이 아니라는 것이다. 기능 구현은 테스트코드 작성이 끝나야 완성이 된다. 백기선님은 포트폴리오를 볼 때 테스트코드가 없으면 코드를 들여다보지조차 않는다고 하는데 그 의미를 어렴풋이 알 것 같았다. 테스트코드는 프로젝트의 완성도를 위해 필수조건이다.
'programming > java, spring' 카테고리의 다른 글
| 트랜잭션 격리수준과 JPA 낙관적 락을 통한 동시성 제어 (4) | 2023.03.11 |
|---|---|
| Hibernate 내부 클래스 PersistentBag (3) | 2023.03.03 |
| Spring Security 인증 내부를 파헤쳐보자 (2) | 2023.02.25 |
| Mockito로 테스트코드 작성하기 (3) | 2023.02.07 |
| JPA Entity가 기본 생성자를 가져야 하는 이유 (정적 팩토리 메소드) (2) | 2022.08.12 |