테스트 커버리지(Test Coverage)
테스트 커버리지(Test Coverage)
테스트는 얼마나 작성해야할까? 이를 어떻게 판단할까?
테스트 커버리지
테스트 커버리지란 시스템 또는 소프트웨어의 테스트를 논할 때 얼마나 테스트가 충분한가를 나타낸 것이다. 즉, 수행한 테스트가 테스트의 대상을 얼마나 커버했는지를 나타낸다.
테스트를 기능에 대한 테스트부터 점점 작은 단위로 내려오다보면 단위 테스트의 경우 클래스, 컴포넌트 단위의 테스트를 하기 때문에 테스트에 대한 커버 범위를 각각의 클래스 또는 소스 코드의 각 라인을 척도로 삼을 수 있게 된다. 이렇게 코드가 얼마나 테스트 됐는지 나타내는 커버리지, 코드 커버리지(구조적 커버리지)라고 한다. 소스 코드를 기반으로 수행하는 화이트박스 테스트를 통해 측정한다.
- 테스트의 충분함을 측정한다.
- 테스트 대상의 전체 범위에서 테스트를 수행한 범위로 측정된다.
- 그렇다고 100%가 달성되면 완벽한 소프트웨어는 아니다.
필요성
우리는 테스트를 진행할 때 얼마만큼을 테스트를 해야하고 언제 테스트를 멈출 지 정량적인 지표가 필요하다. 그렇지 않으면 불필요한 테스트들에서 비용을 사용하게 되고 의미없는 테스트만이 진행될 뿐이다.
또한 테스트는 발생할 수 있는 모든 시나리오에 대해 작성되어야 하는데 매우 복잡한 로직의 경우 개발자가 놓치기 쉽다. 이러한 휴먼 에러를 최대한 방지를 하는 용도로써도 필요하다.
실제로도 많이 사용하는 코드 커버리지
많은 서비스 기업에서는 테스트 코드의 중요성을 인지하고 코드 커버리지를 최대한 유지 및 지속, 상승시키면서 개발을 하려고 노력한다. 코드 커버리지 도구(java의 경우 Cobertuna, Jacoco, Colver가 있다. 비교글)와 소나큐브(SonarQube)와 같은 정적 코드 분석 도구를 함께 활용하여 코드 커버리지가 기존보다 떨어지는 경우 커밋(commit)이 불가능하도록 제한하기도한다.
테스트 커버리지 100%의 함정은 항상 주의해야한다.
코드가 실행된다고 해서 모든 버그들이 제어되는 것이 아니기 때문에 테스트 커버리지 100%는 완벽한 소프트웨어를 나타내지 않는다. 따라서 맹신하면 안되고 주의가 필요하다. 그렇기에 필요(테스트 커버리지와 실질적으로 테스트할 수 있는 비용)에 맞추어 테스트 커버리지를 설정하고 테스트 중단점을 설정해서 테스트를 하자.
코드 커버리지 측정 종류
코드의 구조를 살펴보면 크게 구문(Statement), 조건(Condition), 결정(Decision)의 구조로 이루어져 있다. 코드 커버리지는 이러한 코드의 구조를 얼마나 커버했느냐에 따라 측정 기준을 나눈다.
- 구문 커버리지(Statement Coverage)
- 결정 커버리지(Decision Coverage)
- 조건 커버리지(Condition Coverage)
- 조건/결정 커버리지(Condition/Decision Coverage)
- 변형 조건/결정 커버리지(Modified Condition/Decision Coverage)
- 다중 조건 커버리지(Multiple Condition Coverage)
- 경로 커버리지(All Path Coverage)
코드 커버리지 범위
구문 커버리지(Statement Coverage)
라인(Line) 커버리지라고 부르기도 한다. 코드 한 줄이 한번 이상 실행된다면 충족된다.
void test(int n) {
// 함수 A 실행 - 1번
if (n > 0) { //- 2번
// 함수 B 실행 - 3번
}
// 함수 C 실행 - 4번
}
위에의 코드를 n 이 음수로 들어오는 테스트 하나만 했다고 하자. 그러면 1~4번 구문중 3번 구문이 실행이 안돼서 구문 커버리지는 3 / 4 * 100% = 75%
인 테스트가 된다.
조건 커버리지(Condition Coverage)
여기서의 조건은 모든 조건식을 얘기한다. 내부 조건이 true/false의 경우를 충족하는 지를 본다.
void test(int a, int b) {
// 함수 A 실행 - 1번
if (a > 0 && b < 0) { //- 2번
// 함수 B 실행 - 3번
}
// 함수 C 실행 - 4번
}
여기서 내부 조건이란 조건 식 내부의 각각의 조건이다. 즉, a > 0
과 b < 0
를 얘기하며 각각이 true/false의 경우가 있으면 조건 커버리지를 만족한다.
위에 코드를 테스트하기 위해 a = 1, b = 1
과 a = -1, b = -1
를 넣어보는 테스트를 만들었다고 해보자. 내부 조건식 a > 0
와 b < 0
는 모두 양수일 때와 음수일 때 테스트 케이스가 존재하므로 조건 커버리지를 만족한다.
하지만 a > 0 && b < 0
는 두 경우 모두 false 이므로 함수 B 실행
의 구문이 들어간 경우는 테스트가 안되는 결과가 발생한다.
이처럼 테스트를 작성했을 때 조건 커버리지를 만족하더라도 구문 커버리지와 이후에 나올 결정 커버리지를 만족하지 못하는 경우가 존재한다.
결정 커버리지(Decision Coverage)
브랜치(Branch) 커버리지라고 부르기도 한다. 모든 조건식이 true/false를 가지게 되면 충족된다.
void test(int a, int b) {
// 함수 A 실행 - 1번
if (a > 0 && b < 0) { //- 2번
// 함수 B 실행 - 3번
}
// 함수 C 실행 - 4번
}
조건 커버리지에서의 예시를 다시 가져와보자. a = 1, b = 1
과 a = -1, b = -1
를 넣어보는 테스트를 했을 때 조건 커버리지가 만족되더라도 함수 B
가 실행되지 않았던 결과가 있었다. 결정 커버리지를 만족한다면 이런 문제는 없을 것이다. 그 이유는 결정 커버리지에서 결정은 내부 조건이 아닌 조건식을 얘기한다. 즉, 위에 코드에서는 a > 0 && b < 0
를 얘기한다.
따라서 결정 커버리지를 만족하는 테스트를 만든다고 한다면 a = 1, b = 1
과 a = -1, b = -1
와 같이 조건식이 false만 나오는 식이 아닌 a = 1, b = -1
를 넣는 테스트를 만들어야한다. 이 테스트 케이스를 넣게되면 조건 커버리지와 결정 커버리지, 구문 커버리지를 모두 만족할 수 있게된다.
가장 많이 사용되는 코드 커버리지
위에 세 가지 코드 커버리지 중에서 구문 커버리지가 가장 대표적으로 많이 사용되고 있다.
그 이유는 조건 커버리지나 브랜치 커버리지의 경우 코드 실행에 대한 테스트보다는 로직의 시나리오에 대한 테스트에 더 가깝다고 볼 수 있기 때문이다.
위에 두 커버리지는 조건문이 존재하지 않는 코드의 경우 그 코드는 커버리지 대상에서 아예 제외를 한다. 즉, 해당 코드들을 테스트를 하지 않는다.
그러나 구문 커버리지를 만족한다면, 모든 코드를 테스트 코드가 커버했다고는 말할 수 있는 있게된다. 물론 위에 결정 커버리지의 코드 예시에서 조건식이 false인 시나리오에 대해서 테스트 됐다고 보장할 수 없지만 그래도 조건문 내부의 코드가 실행되었을 때 문제가 없다는 것은 보장할 수 있다.
정리하면, 구문 커버리지를 만족하면 모든 시나리오를 테스트한다는 보장은 할 수 없지만, 어떤 코드가 실행되더라도 해당 코드는 문제가 없다는 보장은 할 수 있다. 이런 이유로 구문 커버리지를 더 많이 사용한다.