경로 커버리지(Path Coverage)

경로 커버리지는 프로그램의 제어 흐름 그래프(Control Flow Graph, CFG)에서 모든 가능한 실행 경로를 테스트하는 구조적 테스팅 기법이다.
이는 프로그램의 입력과 출력 값보다는 내부 제어 흐름에 초점을 맞춘다.

먼저 경로 커버리지의 기본 개념을 간단한 예제를 통해 이해해보자:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def calculate_discount(price, is_member, is_sale_period):
    if is_member:
        if is_sale_period:
            return price * 0.8  # 20% 할인
        else:
            return price * 0.9  # 10% 할인
    else:
        if is_sale_period:
            return price * 0.95  # 5% 할인
        else:
            return price  # 할인 없음

이 함수에는 다음과 같은 가능한 실행 경로들이 있다:

  1. 경로 1: 회원이면서 세일 기간인 경우
  2. 경로 2: 회원이지만 세일 기간이 아닌 경우
  3. 경로 3: 비회원이면서 세일 기간인 경우
  4. 경로 4: 비회원이고 세일 기간도 아닌 경우

이러한 모든 경로를 테스트하기 위한 테스트 케이스를 작성해보자:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def test_calculate_discount():
    # 경로 1: 회원 + 세일 기간
    assert calculate_discount(100, True, True) == 80
    
    # 경로 2: 회원 + 비세일 기간
    assert calculate_discount(100, True, False) == 90
    
    # 경로 3: 비회원 + 세일 기간
    assert calculate_discount(100, False, True) == 95
    
    # 경로 4: 비회원 + 비세일 기간
    assert calculate_discount(100, False, False) == 100

이제 더 복잡한 예제를 통해 경로 커버리지를 알아보자:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class LoanApprovalSystem {
    public String evaluateLoan(int creditScore, double income, boolean hasCollateral) {
        if (creditScore >= 700) {
            if (income >= 50000) {
                return "Approved";
            } else if (hasCollateral) {
                return "Approved with Collateral";
            } else {
                return "Need Higher Income";
            }
        } else if (creditScore >= 600) {
            if (income >= 70000 && hasCollateral) {
                return "Conditionally Approved";
            } else {
                return "Need Improvement";
            }
        } else {
            return "Rejected";
        }
    }
}

이 대출 승인 시스템의 모든 가능한 경로를 테스트하기 위해서는 다음과 같은 테스트 케이스들이 필요하다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Test
void testLoanApproval() {
    LoanApprovalSystem system = new LoanApprovalSystem();
    
    // 경로 1: 높은 신용점수 + 충분한 수입
    assertEquals("Approved", 
        system.evaluateLoan(750, 60000, false));
    
    // 경로 2: 높은 신용점수 + 낮은 수입 + 담보 있음
    assertEquals("Approved with Collateral", 
        system.evaluateLoan(750, 40000, true));
    
    // 경로 3: 높은 신용점수 + 낮은 수입 + 담보 없음
    assertEquals("Need Higher Income", 
        system.evaluateLoan(750, 40000, false));
    
    // 경로 4: 중간 신용점수 + 높은 수입 + 담보 있음
    assertEquals("Conditionally Approved", 
        system.evaluateLoan(650, 80000, true));
    
    // 경로 5: 중간 신용점수 + 조건 불충분
    assertEquals("Need Improvement", 
        system.evaluateLoan(650, 60000, false));
    
    // 경로 6: 낮은 신용점수
    assertEquals("Rejected", 
        system.evaluateLoan(550, 100000, true));
}

경로 커버리지의 계산 방법은 다음과 같다:

1
2
3
4
5
6
경로 커버리지 = (테스트된 경로의 수) / (가능한 총 경로의 수) × 100%

예를 들어, 위의 LoanApprovalSystem 예제에서:
- 총 가능한 경로 수: 6
- 테스트된 경로 수: 6
따라서, 경로 커버리지 = (6/6) × 100% = 100%

경로 커버리지를 달성할 때 주의해야 할 점

  1. 루프가 있는 경우의 처리:

    1
    2
    3
    4
    5
    6
    7
    8
    
    public int sumUntilNegative(int[] numbers) {
        int sum = 0;
        for (int num : numbers) {
            if (num < 0) break;
            sum += num;
        }
        return sum;
    }
    

    이런 경우, 다음과 같은 시나리오를 고려해야 한다:

    • 빈 배열
    • 음수가 없는 배열
    • 첫 번째 요소가 음수인 배열
    • 중간에 음수가 있는 배열
  2. 예외 처리가 있는 경우:

    1
    2
    3
    4
    5
    6
    7
    
    public double divide(int a, int b) {
        try {
            return a / b;
        } catch (ArithmeticException e) {
            throw new IllegalArgumentException("Cannot divide by zero");
        }
    }
    

    이 경우 정상 실행 경로와 예외 발생 경로 모두를 테스트해야 한다.

경로 커버리지의 주요 장점은 다음과 같습니다:

  1. 완전성: 프로그램의 모든 가능한 실행 경로를 검증할 수 있습니다.
  2. 결함 발견: 특정 조건 조합에서만 발생하는 미묘한 버그를 찾아낼 수 있습니다.
  3. 논리적 완전성: 모든 의사결정 경로가 테스트되므로 논리적 오류를 발견하기 쉽습니다.

주요 특징

  1. 모든 가능한 경로 테스트: 프로그램의 모든 가능한 실행 경로를 최소한 한 번씩 테스트한다.
  2. 화이트박스 테스팅: 프로그램의 소스 코드를 분석하여 다양한 경로를 식별한다.
  3. 순환 복잡도(Cyclomatic Complexity) 활용: 프로그램의 복잡도를 측정하여 테스트 케이스 설계에 활용한다.

적용 방법

  1. 코드 이해: 테스트할 코드를 철저히 분석하고 이해한다.
  2. 제어 흐름 그래프(CFG) 작성: 프로그램의 제어 흐름을 그래프로 표현한다.
  3. 경로 식별: CFG에서 모든 가능한 경로를 식별하고 나열한다.
  4. 테스트 케이스 설계: 각 경로를 커버하는 테스트 케이스를 설계한다.
  5. 테스트 실행 및 분석: 설계된 테스트 케이스를 실행하고 결과를 분석한다.

장점

  1. 철저한 테스트: 모든 가능한 경로를 테스트하여 숨겨진 결함을 발견할 수 있다.
  2. 논리적 오류 검출: 프로그램 로직의 오류를 효과적으로 찾아낼 수 있다.
  3. 중복 테스트 감소: 각 코드 라인을 최소한 한 번씩 테스트하므로 중복 테스트를 줄일 수 있다.

한계점

  1. 복잡성: 프로그램의 크기와 복잡도에 따라 가능한 경로의 수가 기하급수적으로 증가할 수 있다.
  2. 시간과 비용: 모든 경로를 테스트하는 데 많은 시간과 자원이 필요할 수 있다.
  3. 실현 가능성: 루프가 있는 경우 무한한 수의 경로 변형이 생길 수 있어 완전한 경로 커버리지를 달성하는 것이 불가능할 수 있다.

경로 커버리지는 이론적으로 가장 강력한 커버리지 메트릭이지만, 실제 적용에는 한계가 있다.
따라서 다른 커버리지 기법들과 함께 사용하여 효과적인 테스팅 전략을 수립하는 것이 중요하다.


참고 및 출처