함수 커버리지 (Function Coverage)

함수 커버리지는 프로그램 내의 모든 함수가 테스트 중에 최소한 한 번 이상 호출되었는지를 측정하는 지표이다.

간단한 예제:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Calculator:
    def add(self, a, b):
        return a + b
    
    def subtract(self, a, b):
        return a - b
    
    def multiply(self, a, b):
        return a * b
    
    def divide(self, a, b):
        if b == 0:
            raise ValueError("Cannot divide by zero")
        return a / b

이 계산기 클래스의 모든 함수를 테스트하기 위해서는 다음과 같은 테스트 코드가 필요하다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def test_calculator():
    calc = Calculator()
    
    # add 함수 테스트
    assert calc.add(5, 3) == 8
    
    # subtract 함수 테스트
    assert calc.subtract(10, 4) == 6
    
    # multiply 함수 테스트
    assert calc.multiply(6, 2) == 12
    
    # divide 함수 테스트
    assert calc.divide(8, 2) == 4

이제 실제 개발에서 자주 마주치는 더 복잡한 예제:

 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 OrderProcessor {
    public void processOrder(Order order) {
        validateOrder(order);
        calculateTotal(order);
        applyDiscounts(order);
        updateInventory(order);
        sendConfirmation(order);
    }
    
    private void validateOrder(Order order) {
        // 주문 유효성 검사
    }
    
    private void calculateTotal(Order order) {
        // 총액 계산
    }
    
    private void applyDiscounts(Order order) {
        // 할인 적용
    }
    
    private void updateInventory(Order order) {
        // 재고 업데이트
    }
    
    private void sendConfirmation(Order order) {
        // 주문 확인 메일 발송
    }
}

이 주문 처리 시스템의 함수 커버리지를 확인하기 위한 테스트를 작성해보자:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Test
void testOrderProcessing() {
    OrderProcessor processor = new OrderProcessor();
    Order testOrder = new Order();
    
    // 모든 함수가 호출되는 시나리오 테스트
    processor.processOrder(testOrder);
    
    // 각 단계가 올바르게 실행되었는지 검증
    verify(testOrder).isValid();          // validateOrder 호출 확인
    verify(testOrder).getTotal();         // calculateTotal 호출 확인
    verify(testOrder).getDiscounts();     // applyDiscounts 호출 확인
    verify(testOrder).getItems();         // updateInventory 호출 확인
    verify(testOrder).getCustomerEmail(); // sendConfirmation 호출 확인
}

함수 커버리지의 계산 방법은 다음과 같다:

1
2
3
4
5
6
함수 커버리지 = (호출된 함수의 수) / (전체 함수의 수) × 100%

예를 들어, OrderProcessor 클래스의 경우:
- 전체 함수 수: 6 (processOrder + 5개의 private 메서드)
- 호출된 함수 수: 6
따라서, 함수 커버리지 = (6/6) × 100% = 100%

함수 커버리지를 측정할 때 특별히 주의해야 할 상황

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class FileHandler {
    public void processFile(String filename) {
        try {
            readFile(filename);
            parseContent();
            saveResults();
        } catch (IOException e) {
            handleError(e);
        } finally {
            cleanup();
        }
    }
    
    private void readFile(String filename) throws IOException {  }
    private void parseContent() {  }
    private void saveResults() {  }
    private void handleError(Exception e) {  }
    private void cleanup() {  }
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Test
void testFileProcessing() {
    FileHandler handler = new FileHandler();
    
    // 정상 경로 테스트
    handler.processFile("valid.txt");
    
    // 예외 경로 테스트
    handler.processFile("invalid.txt");
}

측정 방법

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

함수 커버리지(%) = (호출된 함수의 수 / 전체 함수의 수) × 100

특징

  1. 가장 기본적이고 가벼운 커버리지 측정 방법이다.
  2. 함수의 내부 로직이나 실행 경로는 고려하지 않고, 단순히 함수가 호출되었는지만을 확인한다.
  3. 테스트의 기본적인 수준을 빠르게 확인할 수 있다.

장점

  1. 구현이 간단하고 측정이 빠르다.
  2. 프로그램의 전반적인 테스트 상태를 빠르게 파악할 수 있다.
  3. 테스트되지 않은 함수를 쉽게 식별할 수 있다.

한계점

  1. 함수 내부의 복잡한 로직이나 조건문을 고려하지 않아 테스트의 품질을 정확히 반영하지 못할 수 있다.
  2. 함수가 호출되었다고 해서 그 함수의 모든 기능이 테스트되었다고 보장할 수 없다.
  3. 다른 커버리지 기법들에 비해 테스트의 철저성을 보장하기 어렵다.

이러한 한계를 보완하기 위해, 함수 커버리지는 보통 다른 커버리지 지표들과 함께 사용된다:

  1. 구문 커버리지로 함수 내부의 코드 실행을 확인
  2. 분기 커버리지로 조건문의 다양한 경로를 검증
  3. 통합 테스트로 함수들 간의 상호작용을 확인
    이처럼 여러 테스트 기법을 조합함으로써, 소프트웨어의 품질을 더욱 효과적으로 보장할 수 있다.

참고 및 출처