(IntelliJ 의 코드 커버리지 기능)
코드 커버리지(Code Coverage)
란, 작성한 프로덕션 코드가 테스트에 의해 얼마만큼 실행되었는지를 나타내는 백분율로 나타내는 지표이다. 테스트 역시 (필요한) 비용에 해당하므로, 코드 커버리지를 어떤 테스트를 추가적으로 작성할지, 불필요한 테스트를 작성하고 있지는 않은지 등에 대한 기준점으로 활용해볼 수 있다.
커버리지를 측정할 수 있는 기준은 다양하다. 함수가 실행되었는지 여부를 기준으로 할 수도 있고, 실제 작성한 Statement 개수를 기준으로 할 수도 있다. 여기서는 자주 사용되는 4가지 기준에 대해 소개한다.
함수 커버리지 = 실행된 함수 개수 / 총 함수 개수 * 100
함수 커버리지는, 정의된 함수 중 몇 개나 호출되었는가를 나타내는 지표이다.
아래 예제를 보자.
위와 같이 Calculator
클래스가 있다. 생성자를 제외한 함수는 a, b, c, d 이렇게 4가지이다.
이 4개 중에, a와 b 두 가지만 테스트된다면 정의된 함수 4개 중에 2개만 실행되었으므로 함수 커버리지는 50%이다.
위와 같은 테스트를 작성하고(아무것도 검증하고 있지 않기 때문에 테스트라 말하기는 어렵지만, 개념 설명을 위해 많은 것을 생략한 것으로 이해해주시면 감사하겠습니다.), 커버리지를 측정해보자.
IntelliJ 에서 실행할 테스트 클래스를 우클릭하여, More Run/Debug > Run TestClass with Coverage
를 통해 커버리지를 측정할 수 있다.
Method 항목을 살펴보면, 정의된 함수 4개 중에 2개만 실행되었으므로 함수 커버리지는 50% 임을 알 수 있다. (private 생성자의 경우 카운팅 되지 않는다.)
구문 커버리지 = 실행된 구문 개수 / 총 구문 개수 * 100
라인(Line) 커버리지라고도 한다. 작성한 Statement 중에 얼마나 많은 Statement가 히트되었는지를 나타낸다. (Statement와 Expression에 대한 차이는 여기를 참고.)
설명을 위해, 예제 클래스를 하나의 함수를 갖도록 수정했다. test()
에는 4개의 statement 가 존재한다.
x=1, y=1 을 인자로 주고 테스트를 한다면 (3)번 구문
까지 실행될 것이고 구문 커버리지는 3/4 * 100 = 75%
가 된다.
(Line Coverage = Statement Coverage 가 75% 인 것을 확인할 수 있다.)
구문 커버리지를 100%
로 만들기 위해서는, 모든 구문을 실행하도록 테스트를 작성하면 된다.
예컨대, 위의 경우 x=-1, y=-1
을 입력으로 사용하면 (4)번 구문
까지 실행되므로 커버리지는
4/4 * 100 = 100%
가 된다.
조건 커버리지 = 실행된 조건 개수 / 총 조건 개수 * 100
여기서 말하는 '조건'이란, 조건식 내부의 모든 조건을 말한다.
조건 커버리지는 각 조건이 true/false를 만족시키는지를 확인한다.
public static boolean test(int x, int y) {
System.out.println(x + ", " + y); // (1)
if (x > 0 && y > 0) { // (2)
return true; // (3)
}
return false; // (4)
}
위 예제에서, 조건은 x > 0
, y > 0
이 두 가지가 존재한다.
x=1, y=-1
을 입력하면,x > 0
: truey > 0
: falsex=-1, y=1
을 입력하면,x > 0
: falsey > 0
: true이 되므로, 조건 x > 0
과 y > 0
입장에서는 각각 true/false 를 만족하는 테스트가 수행되었다.
그러나, if 문
입장에서 보면 항상 false 인 테스트만 수행되었으므로 (4)번 구문
은 수행되지 않았다.
즉, 조건 커버리지를 만족하더라도 구문 커버리지는 만족 못하는 경우가 생길 수 있다는 것이다.
브랜치 커버리지 = 실행된 브랜치 개수 / 총 브랜치 개수 * 100
결정(Decision) 커버리지라고도 한다. 프로그램 내의 모든 조건식이 각각 true/false 의 결과를 갖게되면 만족한다.
브랜치 커버리지를 이해하기 위해서는, 먼저 이 '브랜치'가 무엇인지를 정확히 이해해야한다.
public static void printSum(int a, int b) {
int result = a + b; // (1)
if (result > 0) { // (2)
System.out.println("result is positive."); // (3)
} else if (result < 0){ // (4)
System.out.println("result is negative."); // (5)
}
// do nothing
}
위 코드에서 브랜치가 몇 개일까? if
문에 해당하는 경우 한 개, if-else
에 해당하는 경우 한 개. 이렇게 해서 총 2개일까? 아니다. 위 코드의 브랜치는 총 4개이다.
여기서 조건식은 result > 0
, result < 0
이렇게 두 개이며 result > 0
이 true/false 를 갖는 경우의 분기(브랜치) 2개, result < 0
이 true/false 를 갖는 경우의 분기 2개 해서 총 4개가 되는 것이다.
아래 그림을 통해 조금 더 쉽게 이해해보자.
어렵게 생각하지 말자. 말 그대로 분기의 개수를 세는 것이다. 조건문마다 2개의 분기가 이루어지고, 현재 조건문은 2개이므로 브랜치는 2 * 2 = 4개 이다.
IntelliJ 에서도 브랜치가 몇 개인지 직접 확인해볼 수 있다.
edit configuration
검색 및 실행한다.Edit configuration templates
를 선택 > JUnit
선택 > Modify options
선택 Enable branch coverage and test tracking
를 적용 후 Run with Coverage
를 통해 테스트를 실행한다.그럼 방금 전 설명과 마찬가지로, 브랜치 개수가 4개인 것을 확인할 수 있다.
아직 브랜치 개수에 대해 헷갈릴 수도 있으니, 예제를 더 살펴보자.
public static void printSum(int a, int b) {
int result = a + b; // (1)
if (result > 0) { // (2)
System.out.println("result is positive."); // (3)
} else if (result < 0){ // (4)
System.out.println("result is negative."); // (5)
} else if (result == 0) { // (6)
System.out.println("result is Zero"); // (7)
}
// do nothing
}
위 브랜치의 개수는 몇 개일까?
각 조건문 마다 2개 씩 분기되니, 브랜치는 6개이다.
테스트를 다시 수행해보면, IntelliJ 역시 브랜치를 6개로 표시하고 있음을 확인할 수 있다.
public static void printSum(int a, int b) {
int result = a + b; // (1)
if (result > 0) { // (2)
System.out.println("result is positive."); // (3)
} else if (result < 0){ // (4)
System.out.println("result is negative."); // (5)
}
// do nothing
}
다시 원래의 예제로 돌아오자. 위 예제의 브랜치는 4개라고 했다.
그럼, 브랜치가 4개니까 이를 만족시키려면 4개의 테스트가 필요한 것일까?
아니, 그렇지 않다. 실제로 3개의 테스트만 가지고도 브랜치 커버리지를 만족시킬 수 있다.
직관적으로 생각하면 4가지 분기가 있으니까 4개의 테스트가 있어야할 것 같은데, 실제로는 그렇지 않다.
첫 번째 조건식 다음, 바로 두 번째 조건식이 실행되므로, 3개의 입력만으로도 모든 브랜치를 확인할 수 있다.
각 조건식은 여전히 true/false 를 만족시키는 테스트 케이스를 모두 갖게 되므로, 브랜치 커버리지를 만족시키게 되는 것이다.
public static void printSum(int a, int b) {
int result = a + b; // (1)
if (result > 0) { // (2)
System.out.println("result is positive."); // (3)
} else if (result < 0){ // (4)
System.out.println("result is negative."); // (5)
}
// do nothing
}
위 예제를 통해 구문 커버리지와 브랜치 커버리지의 관계를 몇 가지 유추해볼 수 있다.
브랜치 커버리지는 만족시키지 못하지만, 구문 커버리지는 만족시킬 수 있다.
위처럼 각 조건식을 만족시키는 테스트 케이스만 작성해도, 구문 커버리지를 만족시킬 수 있다.
그러나 모든 구문이 실행되었다고 해도, 모든 브랜치가 실행되는 것은 보장할 수 없다.
즉, 구문 커버리지를 모두 만족시켜도, 브랜치 커버리지를 100% 만족시킨다고 할 수는 없는 것이다.
반면에, 브랜치 커버리지를 만족시키는 경우, 100% 구문 커버리지를 만족시킬 수 있다. 모든 브랜치를 만족시키면, 당연히 모든 구문이 실행되기 때문이다. (단, 물론 브랜치가 존재하는 코드에 한해서다.)
그렇다면, 브랜치 커버리지를 구문 커버리지보다 우선순위에 두고 테스트를 작성해야하는 걸까? 어떤 기준을 우선시할 것인지에 대해 마지막 섹션에서 살펴보자.
일단, 함수 커버리지와 조건 커버리지는 후순위이다.
남은 것은 브랜치와 구문이다. 다만 현실적으로 브랜치 커버리지를 사용하기 어려운 몇 가지 이유가 있다.
예를 들어 내 경우 equals & hashcode
를 재정의하고 IDE에 의해 자동완성 시켜놓은 코드까지 테스트하는 것은 테스트 효율성을 떨어뜨린다고 생각해서 이에 대한 테스트는 진행하지 않는다. 이로 인해 브랜치 커버리지는 떨어지게 된다.
앞서, '브랜치 커버리지를 만족시키는 경우 100% 구문 커버리지를 만족시킨다.'라고 했지만 이것은 언제까지나 브랜치가 존재할 경우에 한해서다. 실제 프로덕션 코드에는 브랜치(분기)가 없는 코드가 더 많으므로 이런 코드들은 브랜치 커버리지의 대상에 포함조차 되질 않는다.
(브랜치 커버리지보다 높은 구문(Statement, Line) 커버리지)
실제로 위 같은 이유들로 인해, 진행 중이던 토이 프로젝트의 커버리지를 측정해보니 사진과 같은 결과가 나오는 것을 확인할 수 있었다.
위 브랜치 커버리지를 현실적으로 '무조건' 우선순위로 두기에는 어렵다는 것은 알았으니, 남은 것은 구문 커버리지다. 구문 커버리지를 만족시킨다는 것은 적어도 작성한 코드들이 테스트에 의해 검증되었다는 것은 보장하므로, 우선은 이 편을 사용할 것 같다. ('장애'가 없다는 것까지 보장하지는 못한다.)
물론 브랜치 커버리지와 구문 커버리지를 모두 봐야하는 단위 테스트가 존재할 경우에는 브랜치 커버리지를 높이기 위해 테스트를 조금 더 추가하고. 더 나은 방식이 떠오를 때까지 당분간 이 방법을 써보려고한다.
마침.
잘못된 정보가 있다면 언제든지 말씀 부탁드립니다 :)