구문 커버리지 (Statement Coverage)

구문 커버리지는 프로그램을 구성하는 모든 문장들이 최소한 한 번은 실행될 수 있는 입력 데이터를 테스트 데이터로 선정하는 기준이다.
또한 라인 커버리지(Line Coverage)라고도 불린다.

먼저 간단한 예제를 통해 구문 커버리지의 이해:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def calculate_grade(score):
    # 구문 1
    if score >= 90:
        # 구문 2
        grade = 'A'
    elif score >= 80:
        # 구문 3
        grade = 'B'
    else:
        # 구문 4
        grade = 'C'
    # 구문 5
    return grade

이 함수의 모든 구문을 실행하기 위해서는 다음과 같은 테스트 케이스가 필요하다:

1
2
3
4
5
6
7
8
9
def test_calculate_grade():
    # 구문 1, 2, 5를 실행
    assert calculate_grade(95) == 'A'
    
    # 구문 1, 3, 5를 실행
    assert calculate_grade(85) == 'B'
    
    # 구문 1, 4, 5를 실행
    assert calculate_grade(75) == 'C'

여기서 각 테스트 케이스가 실행하는 구문을 추적하면서 커버리지를 계산할 수 있다.
이 예제에서는 모든 구문(1-5)이 최소 한 번 이상 실행되므로 100% 구문 커버리지를 달성함.

이제 더 복잡한 실제 예제를 통해 구문 커버리지의 중요성을 살펴보자:

 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
29
public class BankAccount {
    private double balance;
    private boolean frozen;

    public boolean withdraw(double amount) {
        // 구문 1
        if (amount <= 0) {
            // 구문 2
            return false;
        }
        
        // 구문 3
        if (frozen) {
            // 구문 4
            return false;
        }
        
        // 구문 5
        if (balance >= amount) {
            // 구문 6
            balance -= amount;
            // 구문 7
            return true;
        }
        
        // 구문 8
        return false;
    }
}

이 은행 계좌 시스템의 모든 구문을 테스트하기 위한 테스트 케이스를 작성해보면:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Test
void testWithdraw() {
    BankAccount account = new BankAccount();
    
    // 음수 금액 테스트 (구문 1, 2)
    assertFalse(account.withdraw(-100));
    
    // 계좌 동결 상태 테스트 (구문 1, 3, 4)
    account.setFrozen(true);
    assertFalse(account.withdraw(50));
    
    // 정상 출금 테스트 (구문 1, 3, 5, 6, 7)
    account.setFrozen(false);
    account.deposit(100);
    assertTrue(account.withdraw(50));
    
    // 잔액 부족 테스트 (구문 1, 3, 5, 8)
    assertFalse(account.withdraw(1000));
}

구문 커버리지의 계산 방법은 다음과 같다:

1
2
3
4
5
6
구문 커버리지 = (실행된 구문의 수) / (전체 구문의 수) × 100%

예를 들어, 위의 BankAccount 예제에서:
- 전체 구문 수: 8
- 실행된 구문 수: 8
따라서, 구문 커버리지 = (8/8) × 100% = 100%

구문 커버리지를 측정할 때 주의해야 할 점점

  1. 예외 처리가 포함된 코드:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    public double divideSafely(int numerator, int denominator) {
        try {
            // 구문 1
            return numerator / denominator;
        } catch (ArithmeticException e) {
            // 구문 2
            System.err.println("Division by zero");
            // 구문 3
            return 0;
        }
    }
    

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

  2. 조건부 실행이 있는 코드:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    public void processTransaction(boolean debug) {
        // 구문 1
        performTransaction();
    
        if (debug) {
            // 구문 2
            logDebugInfo();
        }
    }
    

    디버그 모드가 켜진 경우와 꺼진 경우 모두를 테스트해야 한다.

측정 방법

구문 커버리지는 다음과 같은 공식으로 계산된다:

구문 커버리지(%) = (실행된 구문의 수 / 전체 구문의 수) × 100

특징

  1. 코드의 모든 구문을 실행할 수 있는 입력값이나 이벤트 등의 테스트 데이터를 제공하면 달성된다.
  2. 가장 기본적인 커버리지 측정 방법으로, 다른 커버리지 기법들에 비해 측정 강도가 가장 약하다.
  3. 분기 커버리지, 다중 조건 커버리지, 경로 커버리지 등 포함관계가 더 큰 커버리지를 달성하면 저절로 달성된다.

장점

  1. 적은 개수의 테스트 데이터로 쉽게 달성할 수 있다.
  2. 테스트 진행 정도를 코드의 범위 형태로 표현하기 때문에, 개발자가 커버리지의 의미를 직관적으로 이해할 수 있다.

한계점

  1. 코드 상에 존재하는 가능한 경우 중 많은 부분을 검증하지 못하는 보장성이 낮은 커버리지이다.
  2. 조건문의 모든 경우를 테스트하지 못할 수 있다. 예를 들어, if문의 조건이 참인 경우만 테스트되고 거짓인 경우는 테스트되지 않을 수 있다.

그래서 구문 커버리지는 보통 다른 커버리지 지표들(분기 커버리지, 조건 커버리지 등)과 함께 사용된다.
예를 들어, 다음과 같은 테스트 전략을 수립할 수 있다:

  1. 기본적인 구문 커버리지로 시작하여 실행되지 않는 코드를 찾는다.
  2. 분기 커버리지를 통해 조건문의 다양한 경로를 테스트한다.
  3. 필요한 경우 더 높은 수준의 커버리지(조건, MC/DC 등)를 적용한다.
    이러한 체계적인 접근을 통해 소프트웨어의 품질을 효과적으로 보장할 수 있다.

참고 및 출처