테스트 커버리지 100%의 함정

커버리지를 올리는 작업은 지루하고 힘들다. 하지만 커버리지 100% 달성의 뿌듯함과 성취감은 지루하고 힘든 시간의 보상으로 충분하고도 남는다. 우리가 커버리지의 함정에 빠지기 가장 좋은 순간 역시 100% 커버리지가 달성되는 순간이다. 커버리지가 100%임에도 우리가 기뻐하고 행복해하면 안 되는 이유를 알아보자.


커버리지 100%의 의미

“Bug Free Software!!”를 증명하는 것이 가능할까? 물론 단순한 소프트웨어는 가능할지 모르겠으나 코드의 양이 많아질수록 소프트웨어의 무결성을 증명하는 것은 어려워진다.왜 일까?

완성된 소프트웨어는 복잡계(complex system)의 특성을 가진다.

“복잡계(複雜系, 영어: complex system, complexity system)는 완전한 질서나 완전한 무질서를 보이지 않고, 그 사이에 존재하는 계로써, 수많은 요소들로 구성되어 있으며 그들 사이의 비선형 상호작용에 의해 집단성질이 떠오르는 다체 문제이다 [1]. 최근 자연과학 및 사회과학에서 활발히 연구되고 있다. 물리적, 생물학적, 사회학적 대상을 수학적으로 분석하는 것이 복잡계 과학의 목적이다. 독일의 막스플랑크 연구소와 미국의 산타페 연구소가 복잡계 과학 연구로 유명하다. 위키백과.”

날씨, 뇌에서 일어나는 시냅스들 간의 상호 작용, 박쥐 때의 움직임, 전염병 등등 자연계 대부분의 현상이 복잡계에 속한다.
복잡계의 속성 하나하나는 어느 정도 예측할 수 있다. 하지만 속성들이 서로 상호 작용을 하는 순간 전체 시스템은 카오스 상태에 들어가게 된다. 한 마리 나비의 날갯짓이 돌풍을 일으키듯이 작은 변수의 변화가 전체 시스템의 상태를 변화시킬 수 있는 것이다.

소프트웨어 역시 함수, 변수, 사용자의 행동, OS, 하드웨어 등등…. 수많은 요소로 구성된 일종의 복잡계이다.
수많은 변수가 상호 작용 하면서 돌아가는 소프트웨어의 모든 경로와 출력을 예측하여 빈틈없는 테스트 케이스를 작성하는 것은 거의 불가능에 가깝다.

다시 원점으로 돌아가서 생각해보자. 커버리지 100%의 의미는 무엇일까?
내일 일기예보를 위해 모든 나비의 날갯짓을 전부 확인해야 하는가? 현실적으로 불가능하다. 소프트웨어 역시 마찬가지이다.
소프트웨어의 모든 입력과 변수들 간의 상호 작용을 테스트하려면 거의 영원에 가까운 시간이 걸릴지도 모른다. 언젠가는 테스트를 멈추고 릴리즈를 해야 한다. 이때 사용되는 테스트의 종료 조건이 바로 커버리지 이다.
커버리지 100%는 소프트웨어의 무결성을 증명하는 지표가 아니라 “테스트의 종료 조건을 만족했다.”는 의미이다.
커버리지 100%의 함정이란 바로 커버리지 100%가 소프트웨어 무결성의 충분조건이 아니라는 것을 의미한다.


커버리지 100% 함정의 예

아래 예제는 간단한 요구사항과 산출물인 코드 그리고 해당 코드의 커버리지를 100% 만족하는 케이스이다.

requirements
두 개의 정수와 연산자 하나를 입력받는 간단한 사칙연산(+,-,*,/) 기능을 수행하는 calc 함수를 작성하시오.

source

float calc(int a, int b, char c){
    float result = 0;
    switch (c)
    {
        case '+':
            result = a+b;
            break;
        case '-':
            result = a+b;
            break;
        case '/':
            result = a/b;                
            break;
        default:
            break;
    }
    return result;
}

test cases

  • a=1, b=1, c=‘+’
  • a=1, b=1, c=‘-’
  • a=1, b=1, c=‘*’
  • a=1, b=1, c=‘/’

위의 경우 커버리지 100%이기 때문에 무결성 코드라고 할 수 있을까? 테스트를 종료하고 릴리즈 해야 할까?
위의 코드가 가지고 있는 버그와 누락된 케이스들을 살펴보자.

누락된 기능
calc함수는 요구사항의 사칙연산 중 곱하기 연산이 누락되었다. 누락된 기능에 대한 테스트는 커버리지로 찾아내지 못한다. 커버리지는 코드가 동작한다는 것을 증명할 뿐 요구사항을 모두 구현하였는지는 테스트 할 수 없다.

잘못된 기능
a=1, b=1, c=‘-‘ 인 테스트 케이스의 기대하는 반환 값은 0이다. 하지만 calc에서는 2를 반환한다. 뺄셈이 잘못 구현된 것이다. 커버리지는 대상 소스코드가 요구사항대로 동작하는지는 테스트 할 수 없다.

누락된 케이스
a=1, b=0, c=‘/‘ 인 테스트 케이스는 divide by zero 예외를 발생시킨다. 커버리지는 시스템에 입력 가능한 모든 케이스를 수행했다는 것을 말하지 않는다. 요구사항을 모두 만족하고 커버리지가 100%일 때도 코드의 무결성은 확보되지 않는다. 테스트를 수행할 때는 코드의 문맥과 개발자의 의도 그리고 예외 상황을 꼼꼼히 살펴보아야 한다.


정리

앞서 설명했듯이 소프트웨어는 복잡계의 성질을 가진다. 입력 변수가 3개뿐인 함수도 매우 다양한 결과를 반환한다. 소스코드의 100% 무결성 이란 가능한 모든 입력 변수와 기대되는 모든 반환 값을 살펴보아야 가능하다. 만약 소스코드가 동시성(concurrency)의 특성을 가진다면 가능한 모든 입력 변수를 수행해도 기대되는 반환 값이 다를 수 있기 때문에 100% 무결성을 증명하기 어렵다. 이러한 특성들을 가지는 코드의 커버리지 100%는 모든 경로를 수행해 보았다는 것만을 의미한다. 코드의 모든 경로가 이상 없이 수행된다는 보장은 할 수 없고 요구사항에 맞게 동작하는지도 알 수 없다.

커버리지 100%를 달성한 테스터 혹은 개발자는 충분히 기뻐할 자격이 있다. 그리고 나는 그들의 노력에 주저 없이 박수를 보낼 것이다. 하지만 함정에 빠지지는 말자. 잠시 휴식을 취한 뒤 다시 모니터 앞에 앉아서 더 꼼꼼한 테스트의 그물을 쳐보자. 분명 그물 사이로 빠져나가는 버그들을 발견할 수 있을 것이다.