DevOps/개념

테스트 커버리지 이해와 실습 - Code Coverage와 Branch Coverage

가나무마 2025. 7. 4. 16:27

테스트 코드를 작성할 때 가장 중요한 지표 중 하나는 테스트 커버리지(Test Coverage)입니다. 이 지표는 테스트가 실제 코드의 몇 %를 실행하고 있는지를 보여주며, 코드의 신뢰성과 안정성 확보에 직결됩니다.


1. Code Coverage란?

Code Coverage는 테스트 코드가 전체 코드 중 얼마나 많은 부분을 실행하는지를 측정하는 지표입니다.
보통 “실행된 라인의 비율”로 계산되며, 일반적으로 다음과 같은 코드 유형을 기준으로 분류합니다:

Syntax Line 함수 정의, 닫는 중괄호 등 구조적인 코드 (예: def, return)
Logic Line 변수 연산, 값 처리, 실제 동작하는 라인
Branch Line 조건문(if, for, while)으로 분기되는 라인

 

예제 코드

def some_function(n):         # Line 1: Syntax
    result = []               # Line 2: Logic
    for i in range(n):        # Line 3: Branch
        result.append(i)      # Line 4: Logic
        if i == 10:           # Line 5: Branch
            result.append(f"10th line : {i}")  # Line 6: Logic
    return result             # Line 7: Syntax

이 코드에서 Non-Syntax 라인은 다음 6줄입니다:
[2, 3, 4, 5, 6, 7] → 여기서 Line 1과 7은 구조적인 Syntax 요소로 계산에서 제외합니다.

Code Coverage 공식

Code Coverage = (실제로 실행된 Non-Syntax 라인 수) / (전체 Non-Syntax 라인 수)

2. 테스트 코드 예제

import unittest

class TestSomeFunction(unittest.TestCase):
    def test_some_function(self):
        n = 5
        result = some_function(n)
        self.assertEqual([0,1,2,3,4], result)

    def test_some_function_when_n_is_bigger_than_10(self):
        n = 11
        result = some_function(n)
        self.assertIn("10th line : 10", result)

test_some_function의 커버리지 분석

  • n = 5이므로 i == 10 조건은 실행되지 않음.
  • 따라서 Line 6은 실행되지 않음.
  • 총 6줄에서 5줄을 실행하므로 5/6 ≈ 0.83...

test_some_function_when_n_is_bigger_than_10의 커버리지 분석

  • n = 11이므로 모든 분기 조건을 실행.
  • 전체 Non-Syntax 라인 모두 실행됨.
  • 총 6줄에서 6줄을 실행하므로 6/6 = 1.0

3. Branch Coverage란?

Branch Coverage는 코드 라인이 아닌, 조건 분기(Branch) 기준으로 커버리지를 측정합니다.
모든 조건이 참/거짓, 반복 여부 등 다양한 경로로 실행되었는지를 보는 것이 핵심입니다.

예제 코드에서 Branch 구분 찾기

def some_function(n):         # Line 1: Syntax
    result = []               # Line 2: Logic
    for i in range(n):        # Line 3: Branch
        result.append(i)      # Line 4: Logic
        if i == 10:           # Line 5: Branch
            result.append(f"10th line : {i}")  # Line 6: Logic
    return result             # Line 7: Syntax

위 함수에선 총 2개 branch 구분이 존재합니다.

첫 번째로 3번째 줄인 for문 반복 분기 그리고 두 번째로 if i == 10:인 조건문 분기가 있습니다.

 

여기서 다시 테스트 함수 코드를 확인해보겠습니다.

import unittest

class TestSomeFunction(unittest.TestCase):
    def test_some_function(self):
        n = 5
        result = some_function(n)
        self.assertEqual([0,1,2,3,4], result)

    def test_some_function_when_n_is_bigger_than_10(self):
        n = 11
        result = some_function(n)
        self.assertIn("10th line : 10", result)

test_some_function은 some_function의 반복문을 실행하지만 조건문은 n이 5이므로 실행하지 않습니다.

test_some_function_when_n_is_bigger_than_10은 some_function의 반복문도 실행하고 조건문도 실행합니다.

4. 커버리지가 중요한 이유

  • 테스트 코드가 특정 조건만 만족할 때만 통과되는 구조라면 예외 상황에서 버그가 숨어 있을 가능성이 매우 높습니다.
  • 커버리지를 고려하여 테스트하면 예상하지 못한 로직도 실행되어 테스트의 신뢰도를 높일 수 있습니다.
  • 커버리지가 낮다면 추가 테스트를 작성이 필요합니다.

5. 실습

실습은 python unittest와 coverage를 통해서 진행합니다.

전 실습을 간편하게 하기 위해서 Makefile을 정의하였습니다.

.PHONY: test branch_test clean

# 테스트 실행 및 커버리지 측정
test:
	coverage run -m unittest discover -s tests
	coverage report
	coverage html
	@echo "HTML report created at htmlcov/index.html"

branch_test:
	coverage run --branch -m unittest discover -s tests
	coverage report
	coverage html
	@echo "HTML report created at htmlcov/index.html"

# 커버리지 및 캐시 제거
clean:
	rm -rf htmlcov .coverage __pycache__ .pytest_cache

 

이제 make test와 make branch_test를 실행하여 코드 커버리지와 브랜치 커버리지를 확인해보겠습니다.

우선, Code Coverage를 먼저 확인하겠습니다.

Code Coverage 검사

utils/regexs.py 테스트에 1줄이 실행되지 않은 것을 확인할 수 있습니다.

출력된 index.html 파일에 들어가서 결과를 자세하게 살펴보겠습니다.

 

Code Coverage 결과

is_integer 함수의 False가 불리지 않은 것을 볼 수 있습니다. 테스트 코드를 확인해보겠습니다.

 

class UtilTest(unittest.TestCase):
    def test_when_number_is_given_then_return_true(self):
        n = 0
        result = is_integer(n)
        self.assertTrue(result)

테스트로 숫자 값만 주어지므로 얼리 리턴으로 인해 마지막 줄이 실행되지 않습니다.

이제 코드 커버리지를 보완하기 위해 문자열을 넘기는 테스트 코드를 추가하겠습니다.

 

class UtilTest(unittest.TestCase):
    def test_when_number_is_given_then_return_true(self):
        n = 0
        result = is_integer(n)
        self.assertTrue(result)

    def test_when_non_number_type_is_given_then_return_false(self):
        n = "hello world"
        result = is_integer(n)
        self.assertFalse(result)

파라미터를 문자열로 넘기는 테스트 코드를 작성했습니다.

 

테스트 커버리지 재측정. 100%

코드 커버리지가 100%가 됐습니다.

 

6. 결론

코드 커버리지는 테스트를 통해 실제 실행된 코드의 비율을 나타내는 지표

대표적으로 다음과 같은 종류가 있다:

  • Line Coverage: 실행 가능한 코드 줄이 테스트 중 실행되었는지 측정
  • Branch Coverage: 조건문이나 분기문의 각 경로가 모두 테스트되었는지 측정