테스트 커버리지 이해와 실습 - Code Coverage와 Branch Coverage
테스트 코드를 작성할 때 가장 중요한 지표 중 하나는 테스트 커버리지(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를 먼저 확인하겠습니다.
utils/regexs.py 테스트에 1줄이 실행되지 않은 것을 확인할 수 있습니다.
출력된 index.html 파일에 들어가서 결과를 자세하게 살펴보겠습니다.
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%가 됐습니다.
6. 결론
코드 커버리지는 테스트를 통해 실제 실행된 코드의 비율을 나타내는 지표
대표적으로 다음과 같은 종류가 있다:
- Line Coverage: 실행 가능한 코드 줄이 테스트 중 실행되었는지 측정
- Branch Coverage: 조건문이나 분기문의 각 경로가 모두 테스트되었는지 측정