[Slash 21] 테스트 커버리지 100% 정리

구경회·2021년 5월 17일
0
post-thumbnail

어떤 발표인지

문제가 있는 코드는 배포되어서는 안된다. 컴파일 타임에 잡아낼 수 있다면 가장 좋고, 테스트 중 잡아내도 좋다. 어찌되었든 배포 이전에 잡아내야한다. 그렇다면, 어떻게? 그 기준으로 단순명료한 "테스트 커버리지 100%"를 발표자는 제안한다. 영상은 다음 링크에서 볼 수 있다.
https://toss.im/slash-21/sessions/1-6
발표자인 이응준님은 그런 REST API로 괜찮은가라는 유명한 발표를 하셨던 분이다. HTTP 완벽 가이드의 공동 번역자이기도 하다.

왜 100%인가

단순하다

100%는 다른 수치에 비해 단순하다. 90%, 95%등의 수치에 비해 명확한 길라잡이가 될 수 있다. 90%나 95%등 100%가 아닌 다른 수치의 경우 꽤나 골치아픈 경우를 만들 수 있는데, 개발자가 다른 기능 추가 없이 특정 라인을 삭제하는 것만으로 테스트 커버리지를 떨어뜨릴 수 있기 때문이다.
예를 들어 지금 코드가 100줄이고, 99줄이 커버되어 있다고 하자. 테스트 커버리지 하한은 99%로 두자. 이 때 잘 굴러가는 코드 한 줄을 삭제할 경우 하한선을 맞출 수 없어 새로 테스트를 작성해야 한다. 자신이 작성하지 않은 코드에 대한 테스트 책임을 갖게 되는 것이다.

배포와 리팩토링의 자신감

모든 코드는 테스트에 의해 커버되고 있기 때문에 배포와 리팩토링에 자신감을 가질 수 있다.

불필요 프로덕션 사라짐

불필요한 코드는 테스트의 대상이 된다. 테스트를 작성하는 지리한 작업을 거치지 않으려면 불필요한 프로덕션 코드를 삭제할 수 밖에 없다.

이해도 상승

테스트를 작성함으로써 코드를 좀 더 내밀하게 이해할 수 있고 차후에 코드를 살펴볼 때도 테스트를 기준으로 삼아 이해할 수 있다.

점점 쉬워짐

이미 작성한 코드를 참고하여 새로운 테스트를 작성할 수 있고, 테스트를 짜면 짤수록 코드는 테스트하기 쉬워진다.
레거시 시스템에 테스트 코드를 작성한다고 해보자. 분명히 테스트하기 어려울 것이다. 어째서? 처음부터 테스트를 염두에 두고 짜지 않았기 때문에 아주 거대한 함수를 만나거나 사이드이펙트로 점철된 함수를 만나게 되고 이는 테스트하기 어려운 코드들이다. 반면 테스트 주도 개발 등으로 개발을 시작한다면? 처음부터 테스트를 염두에 두고 작성했기 때문에 훨씬 테스트하기 쉬운 코드가 나오게 된다.

어려움

느려지는 테스트

400개 초과, Spring Application Context loading

스프링 애플리케이션 컨텍스트가 테스트가 느려지는 주범이었다. 컨텍스트 로딩 없이 이용할 수 있는 WebTestClient등을 이용해 문제를 해결했다. 제거가 어려운 경우 모킹 오프젝트를 만들어 문제를 해결했다.

1600개 초과

  • SLF4J 초기화 -> 불필요한 로깅 제거
  • Jackson -> Gson으로 교체
  • HandleBars 컴파일 -> handleBars 캐시 적용
  • ByteBuddy 초기화 -> 테스트에서 사용 중단
  • Kotlin Reflection 모듈 초기화 -> isSubClassOf 함수 제거
  • MockK -> 필수적이지 않으면 제거
  • 순차적 실행 -> 클래스 단위 병렬 실행
    추가로 신형 장비(ㅎㅎ)의 도입을 통해 더 빠르게 테스트를 작성할 수 있었다.

테스트하기 어려운 코드

DB, 네트워크, 시간 의존 코드의 경우 모킹으로 어떻게든 테스트가 가능하다. 하지만 코틀린이 생성한 바이트코드는 테스트하기 어렵다. 가령 다음과 같은 elvis operator를 보자.

data class Person(
  val name: String
)

fun getName(person: Person?) = person?.name ?: "아무개"

@Test
fun test() {
  getName(Person("김토스")) shouldBe "김토스"
  getName(Person(null)) shouldBe "아무개"
}

위 코드는 25%의 커버리지를 가진다. 생성된 바이트 코드는 코틀린 프로그래밍 상에서 닿을 수 없는 부분까지 만들어내기 때문이다.
이런 경우 두 가지 선택지가 있다. 1. 포기하거나 2.가독성을 일부 희생하거나
2를 선택한다고 하면 위와 같은 코드를 if~else 등의 구문으로 바꾸어야 할 것이다.

100% 너머

테스트 잘못 작성

fun sum(a: Int, b: Int): Int = a - b
@Test
fun test() {
  sum(0, 0) shouldBe 0
  sum(1, 0) shouldBe 1
}  

위의 코드는 테스트 커버리지 100%를 기록한다. 하지만 위 함수는 잘못된 게 분명하다. 이 때 MutationTest를 도입하면 무제를 해결할 수 있다. 프로덕션 코드를 무작위로 조작해 테스트가 통과하면 테스트케이스가 부족하다고 판단하는 형식이다. 하지만 속도가 매우 느리므로 전면적 도입은 힘들다. 꼭 필요하고 중요한 로직만 해당 테스트를 이용하는 것이 좋을 것이다.

요구사항 오해

기획 단의 요구사항을 오해하여 코드를 잘못 작성할 수 있다. 스펙 문서를 다시 만들어 확인 방법 받는 방법도 있겠으나 드는 품이 너무 크다.

컴포넌트 협업 실패

Pactflow라는 계약 관리 도구 등을 이용하여 문제를 해결할 수 있다.

정리

  • 테스트커버리지는 높일 수 있다
  • 낮으면 빌드를 실패하게 하자
  • 테스트는 빨라야 한다
  • 100%라도 버그는 있을 수 있다
profile
즐기는 거야

0개의 댓글