커버 불가능한 코드

테스트 커버리지를 100%까지 끌어올리면서 테스트로 커버하기가 굉장히 어려운 코드를 종종 만나곤 했다. 다음과 같은 코틀린 코드가 그렇다.

    fun isYes(answer: String): Boolean {
        return when(answer) {
            "yes" -> true
            else -> false
        }
    }

주어진 문자열이 “yes”면 true를 반환하고 아니면 false를 반환할뿐인 지극히 단순한 함수다. 그런데 이 코드를 빠짐없이 테스트로 커버하려고 하면 상당히 어렵다.

그냥 얼핏 보기에는 answer가 “yes”인 경우와 아닌 경우만 테스트하면 될 것 같지만 그렇게 해서는 브랜치 커버리지나 인스트럭션 커버리지 100%가 나오지 않는다. 어째서일까?

그것은 코틀린 when 문의 동작 때문이다. 위 코틀린 코드를 바이트코드로 변환한 뒤 다시 자바로 디컴파일해보면 이런 코드가 나온다.

   public final boolean isYes(@NotNull String answer) {
      Intrinsics.checkParameterIsNotNull(answer, "answer");
      boolean var10000;
      switch(answer.hashCode()) {
      case 119527:
         if (answer.equals("yes")) {
            var10000 = true;
            break;
         }
      default:
         var10000 = false;
      }

      return var10000;
   }

위와 같이, 코틀린의 when 문은 문자열을 직접 비교하는 것이 아니라, 우선 해시코드를 얻은 뒤 해시코드가 같은 경우에만 문자열 비교를 한다. 따라서 모든 분기를 빠짐없이 테스트하려면, “yes”와 해시코드만 같은 다른 문자열을 찾아서 그 경우 함수가 false를 리턴함을 보이는 테스트를 작성해야만한다. 해시코드만 같은 서로 다른 문자열을 찾아내는 것은 물론 매우 어려우므로 현실적으로 저런 코드를 테스트로 100% 커버하는 것은 무리다.

따라서 다음과 같이 when 문을 쓰지 않는 코드로 바꾸는 것이 최선이다.

    fun isYes(answer: String): Boolean {
        return answer == "yes"
    }

코틀린은 코드를 작성하는 개발자에게 편리한 기능을 많이 제공해주는 한편, 바이트코드로 컴파일하면 이와 같이 유저에게 드러나지 않는 분기문 역시 많이 만들어낸다. jvm 플랫폼에서 가장 널리 쓰이는 테스트 커버리지 도구인 jacoco는 브랜치 커버리지를 계산할 때 바이트코드 기준으로 계산하기 때문에 코틀린 언어를 사용하는 개발자가 눈치채지 못한 브랜치도 커버할 것을 요구할 수 있다. 따라서 코틀린 개발자가 테스트 커버리지 100%를 맞추려고 시도한다면 위와 같은 경우 디컴파일을 해 보아야 하는 경우가 있고, 더 운이 나쁘면 디컴파일에 실패해서 바이트코드를 직접 보고 어떤 브랜치가 있는지 확인해야 하는 경우도 있다.

그리고 그렇게 해서 숨겨진 브랜치를 찾아낸다고 하더라도, 이 글의 예와 같이 프로덕션 코드를 고치지 않고서는 도저히 테스트 커버가 불가능한 경우를 만날 수 있다. 그리고 그렇게 고치고 나면 가독성이 더 내려갈수도 있다.

예를 들어 다음과 같은 코드를,

    when(number: String) {
        "one" -> 1
        "two" -> 2
        "three" -> 3
        else -> throw IllegalArgumentException()
    }

다음처럼 보기 싫게 고쳐야 할 수도 있다.

    if (number == "one") {
        1
    } else if (number == "two") {
        2
    } else if (number == "three") {
        3
    } else {
        throw IllegalArgumentException()
    }

이런 상황이 오면 개발자가 결정을 내려야한다. 가독성을 희생하고 커버리지를 올릴지 커버리지를 포기하고 가독성을 지킬지. 나는 가독성과 커버리지를 모두 얻기 위해 많은 시간을 들여 애를 쓴 뒤, 둘 중의 하나는 포기해야함을 깨닫고 모든 코드가 테스트된다는 단순한 규칙을 유지하기 위해 가독성을 일부 희생했다.

어느쪽이 나은지는 각자의 상황에 따라 다르겠지만, 커버리지는 가독성이든 개발자가 반드시 지켜야 할 핵심가치는 아닐 것이다. 완벽하게 하려 너무 애쓸 필요는 없다.

커버 불가능한 코드”에 대한 2개의 생각

  1. 안녕하세요 저는 http완벽가이드 읽는법 << 이 글을 보고 이 블로그를 처음 방문하게 되었는데
    혹시 운영체제의 정석으로 불리는 흔히 말하는 공룡책도 웹개발자 입장에서 필수적인부분과 상대적으로 당장 안읽어도 되는부분 이런식으루 가르쳐주실 수 있을까요?
    첫회독이라서 처음부터 끝까지 완독하기에는 영어원서 + 두께가 장난이 아니네요.
    그리고 웹개발자에게 운영체제는 어느정도로 중요한가요?

    좋아요

댓글 남기기