코드 커버리지
고친 사람 github-actions[bot]
코드 커버리지는 프로그램이 도는 동안 코드의 어디가 실행됐는지 기록해 줍니다. 기록을 모으면 한 번도 실행되지 않은 줄과 갈림길이 드러납니다. 흔히 테스트가 코드를 얼마나 지나갔는지 볼 때 씁니다. 실행된 코드가 맞게 동작했는지는 알려 주지 않습니다.
쉽고 빠른 이해
무슨 일을 하나 — 프로그램이 돌 때 코드의 어느 줄을 지나갔는지 표시해 둡니다. 테스트를 돌린 뒤 보고서를 열면 한 번도 안 지나간 줄이 다른 색으로 보입니다.
왜 하나 — 테스트가 전부 통과해도 테스트가 안 닿은 코드는 깨져 있어도 모릅니다. 어디가 비었는지 알아야 테스트를 더 쓸 곳을 고를 수 있습니다.
어떻게 도나
- 도구가 코드 곳곳에 「여기를 지나갔다」를 적는 짧은 코드를 끼워 넣습니다
- 테스트든 다른 실행이든 평소처럼 프로그램을 돌립니다
- 쌓인 표시를 모아 지나간 줄과 안 지나간 줄을 보여 줍니다
대가 — 표시를 남기는 일이 더해져 실행이 느려집니다. 또 지나간 것만 알 뿐 결과가 맞았는지는 모릅니다. 숫자가 높아도 테스트가 허술할 수 있습니다.
상세
이 절은 코드 커버리지가 무엇을 기록하는지 봅니다. 그 기록을 얻는 방법과 쓰는 곳도 봅니다. 배송비를 정하는 작은 함수 하나를 처음부터 끝까지 들고 갑니다.
낯선 동네를 걸으면서 지나간 길을 지도에 색연필로 칠한다고 해 봅시다. 하루가 끝나면 칠하지 않은 골목이 한눈에 보입니다. 그 골목은 오늘 한 번도 들어가 보지 않은 골목입니다.
코드 커버리지는 프로그램이 도는 동안 실행된 코드를 기록합니다. 그 기록을 전체 코드에 견준 비율도 코드 커버리지라고 부릅니다. 함수가 열 줄인데 실행이 일곱 줄을 지나갔다면 커버리지는 열 줄 중 일곱 줄입니다. 나머지 세 줄은 이번 실행이 한 번도 닿지 않은 코드입니다.
이 기록이 필요한 것은 테스트 결과가 테스트가 지나간 코드에 대해서만 말해 주기 때문입니다. 함수 하나하나를 따로 떼어 확인하는 단위 테스트가 전부 통과해도, 테스트가 안 닿은 코드는 깨져 있을 수 있습니다. 코드 커버리지는 그 닿지 않은 코드를 줄 단위로 짚어 줍니다.
예로 쓸 배송비 함수
함수가 따르는 배송비 규칙은 둘입니다. 주문 금액이 5만 원 이상이면 배송비가 없습니다. 회원이면 배송비를 절반으로 깎습니다.
오른쪽 주석은 아래 실행 한 번에서 그 줄이 실행됐는지를 적은 것입니다.
def shipping_fee(total, member):
fee = 3000 # 지나감
if total >= 50000: # 지나감
fee = 0 # 안 지나감
if member: # 지나감
fee = fee // 2 # 지나감
return fee # 지나감
실행은 2만 원짜리 회원 주문 하나입니다. 돌려받는 값은 오른쪽 주석에 적었습니다.
shipping_fee(20000, True) # 1500
2만 원은 5만 원에 못 미치므로 fee = 0 줄은 건너뜁니다. 회원이므로 3천 원의 절반인 1500 이 됩니다. 몸통 여섯 줄 가운데 다섯 줄을 지나갔습니다.
세는 단위에 따라 달라지는 숫자
같은 실행 한 번도 무엇을 세느냐에 따라 커버리지 숫자가 달라집니다.
가장 쉬운 단위는 줄입니다. 실행된 줄의 비율을 라인 커버리지라고 합니다. 앞의 실행은 여섯 줄 중 다섯 줄을 지났습니다.
if 문은 조건이 참일 때와 거짓일 때 두 방향으로 갈립니다. 이 방향 하나하나를 분기라고 합니다. 지나간 분기의 비율이 분기 커버리지입니다.
배송비 함수에는 if 가 둘이라 분기가 넷입니다. 앞의 실행은 금액 조건에서 거짓 쪽, 회원 조건에서 참 쪽만 지났습니다. 그래서 분기 커버리지는 넷 중 둘입니다.
flowchart TD
A["fee = 3000"] --> B{"total >= 50000"}
B -.->|"참 · 안 지나감"| C["fee = 0"]
B -->|"거짓 · 지나감"| D{"member"}
C -.-> D
D -->|"참 · 지나감"| E["fee = fee // 2"]
D -.->|"거짓 · 안 지나감"| F["return fee"]
E --> F
그림의 점선이 이번 실행이 안 간 길입니다. 라인 커버리지는 fee = 0 한 줄이 비었다고만 알려 줍니다. 분기 커버리지는 회원이 아닌 방향도 비었다고 알려 줍니다. 그 방향에는 자기 몫의 줄이 없어서 줄만 세면 보이지 않습니다.
아래 표는 흔히 보는 단위 셋에 앞의 실행 한 번으로 잰 값을 붙였습니다. 함수 단위는 한 번이라도 불린 함수를 셉니다.
| 단위 | 세는 것 | 앞 실행의 값 |
|---|---|---|
| 함수 커버리지 | 한 번이라도 불린 함수 | 1개 중 1개 |
| 라인 커버리지 | 실행된 줄 | 6줄 중 5줄 |
| 분기 커버리지 | 지나간 분기 | 4개 중 2개 |
표에서 볼 것은 셋째 칸입니다. 실행은 같습니다. 함수로 세면 꽉 찹니다. 분기로 세면 절반입니다. 그래서 커버리지 숫자는 어느 단위로 잰 것인지를 함께 밝혀야 뜻이 섭니다.
분기보다 더 잘게 세는 기준도 있습니다. 그중 경로 커버리지는 함수 처음부터 끝까지 갈 수 있는 길을 셉니다. 배송비 함수라면 if 둘의 참·거짓을 조합한 길 넷을 모두 지났는지 셉니다. 앞의 실행은 그 가운데 하나를 지났습니다.
잘게 셀수록 다 채우기 어려워집니다. 그래서 실무에서는 대개 라인과 분기를 봅니다.
기록을 얻는 방법
도구는 「이 줄이 실행됐다」를 어떻게 알까요? 배송비 함수에 도구가 덧붙이는 코드로 봅니다.
동작을 지켜보려고 프로그램에 기록용 코드를 덧붙이는 일을 계측이라고 합니다. 커버리지 도구는 줄마다, 분기마다 카운터를 하나씩 둡니다. 실행이 그 지점을 지날 때마다 카운터가 하나 오릅니다.
아래는 계측된 배송비 함수의 앞부분을 사람이 읽기 쉽게 풀어 쓴 것입니다. 오른쪽 주석은 앞의 실행 한 번이 끝났을 때 카운터 값입니다.
hit[1] += 1 # 1
fee = 3000
if total >= 50000:
hit[2] += 1 # 0
fee = 0
실행이 끝나면 도구가 카운터를 모읍니다. 0 인 카운터가 안 지나간 줄입니다. 원래 코드가 하는 일은 바뀌지 않습니다.
카운터를 끼워 넣는 시점은 언어에 따라 셋으로 갈립니다.
| 방식 | 언제 끼워 넣나 |
|---|---|
| [[원천 | 소스]] 계측 |
| 결과물 계측 | 컴파일이 끝난 결과물에 카운터를 넣는다. 결과물은 기계어이거나, [[Java |
| 실행기 [[트레이스 | 추적]] |
어느 방식이든 얻는 것은 같습니다. 지점마다 몇 번 지나갔는지가 남습니다.
카운터를 올리는 일이 더해지므로 실행이 느려집니다. 이렇게 늘어나는 부담을 오버헤드라고 합니다. 그래서 커버리지는 대개 테스트를 돌릴 때만 켭니다. 서비스를 운영할 때는 끕니다.
여러 실행의 기록 합치기
커버리지 기록은 실행마다 따로 쌓입니다. 따로 쌓인 기록은 나중에 합칠 수 있습니다.
앞의 실행에 비회원 5만 원 주문 하나를 더 돌린다고 해 봅시다. 이 실행은 금액 조건의 참 쪽과 회원 조건의 거짓 쪽을 지납니다. 두 실행의 카운터를 더하면 0 인 카운터가 하나도 남지 않습니다.
shipping_fee(20000, True) # 1500
shipping_fee(50000, False) # 0
합치는 규칙은 카운터를 더하는 것입니다. 그래서 단위 테스트와 여러 부품을 묶어 확인하는 통합 테스트를 따로 돌려도 기록을 합칠 수 있습니다. 둘 중 하나라도 지나간 줄은 지나간 줄로 칠해집니다.
합친 기록은 커버리지 보고서로 나옵니다. 보고서는 파일마다 비율을 보여 줍니다. 줄마다 지나갔는지도 색으로 칠합니다. 사람이 빈 곳을 찾을 때 여는 것이 이 보고서입니다.
테스트 밖의 쓰임
코드 커버리지는 테스트가 아닌 실행에도 씁니다. 커버리지는 실행이면 무엇이든 잴 수 있습니다. 아래는 그런 쓰임 둘입니다.
프로그램을 실행하면서 살피는 분석을 동적 분석이라고 합니다. 동적 분석은 실행이 지나간 코드만 검사합니다. 코드 커버리지는 그 분석이 코드의 어디까지 닿았는지를 알려 줍니다.
입력을 자동으로 조금씩 바꿔 가며 수없이 넣어 보는 검사가 퍼징입니다. 퍼징 도구 가운데 많은 수가 입력마다 커버리지를 봅니다. 새 분기를 지난 입력은 버리지 않고 다음 변형의 출발점으로 삼습니다.
이렇게 커버리지를 보며 입력을 고르는 방식을 커버리지 유도 퍼징이라고 부릅니다. 이때 커버리지는 사람이 읽는 보고서가 아닙니다. 도구가 다음 입력을 고르는 데 쓰는 신호입니다.
지나간 것과 확인한 것의 차이
커버리지가 꽉 차도 테스트는 허술할 수 있습니다. 배송비 함수를 부르는 테스트 두 개를 견줘 그 까닭을 봅니다.
테스트는 코드를 부르는 데서 끝나지 않습니다. 돌려받은 값이 기대한 값과 같은지 확인해야 합니다. 이 확인 문장을 단언이라고 합니다. 아래 첫째 테스트에는 단언이 있습니다.
fee = shipping_fee(20000, True) # 1500
assert fee == 1500 # 통과
둘째 테스트는 함수를 부르기만 하고 돌려받은 값을 보지 않습니다.
shipping_fee(20000, True) # 1500
두 테스트는 같은 줄과 같은 분기를 지나므로 커버리지가 같습니다. 그런데 둘째 테스트는 함수가 3000 을 돌려줘도 통과합니다. 값을 확인하지 않기 때문입니다.
커버리지는 코드가 실행됐는지를 셉니다. 결과가 맞았는지는 보지 않습니다. 커버리지가 높다는 것은 「테스트가 이 코드를 지나갔다」까지만 말해 줍니다.
없는 코드도 보지 못합니다. 음수 금액을 막는 검사가 필요한데 아무도 안 썼다고 해 봅시다. 커버리지는 있는 줄만 세므로 빠진 검사는 보고서 어디에도 나타나지 않습니다.
테스트가 결함을 잡는 힘을 따로 재는 방법으로 뮤테이션 테스트가 있습니다. 코드를 일부러 조금 바꾼 뒤 테스트가 실패하는지 봅니다. total >= 50000 을 total > 50000 으로 바꿔 봅니다. 그래도 테스트가 통과한다면 그 테스트는 5만 원 경계를 확인하지 않는 것입니다.
커버리지 숫자를 합격선으로 쓸 때
팀이 커버리지 숫자를 합격선으로 걸면 테스트를 쓰는 방식이 바뀝니다.
코드를 올릴 때마다 서버가 빌드하고 테스트를 돌려 주는 방식을 지속적 통합이라고 합니다. 이때 커버리지도 함께 재서 하한을 걸어 두는 팀이 많습니다. 하한을 걸면 커버리지를 그 아래로 떨어뜨리는 변경은 받지 않습니다.
숫자가 목표가 되면 숫자를 올리기 쉬운 테스트가 늘어납니다. 단언 없이 함수를 부르기만 하는 테스트는 쓰기 쉽고 커버리지를 빠르게 올립니다. 그렇게 오른 숫자는 테스트가 튼튼해졌다는 뜻이 아닙니다.
재는 값을 목표로 삼으면 그 값이 더는 본래 재려던 것을 가리키지 못합니다. 이 관찰을 굿하트의 법칙이라고 부릅니다.
보고서의 다른 쓰임은 빈 곳을 찾는 지도입니다. 안 지나간 줄을 하나씩 보며 테스트가 없는 까닭을 묻습니다. 일부러 안 쓴 것인지, 잊은 것인지가 거기서 갈립니다.
테스트 커버리지와 코드 커버리지
테스트 커버리지와 코드 커버리지는 이름이 비슷합니다. 두 말은 많이 겹칩니다. 갈리는 곳은 무엇을 기준으로 세느냐입니다.
테스트를 돌리면서 잰 코드 커버리지를 실무에서는 흔히 테스트 커버리지라고 부릅니다. 두 말을 섞어 써도 대개 뜻이 통하는 까닭이 이것입니다.
테스트 커버리지는 더 넓게 쓰이기도 합니다. 요구사항 가운데 테스트가 붙은 것의 비율을 세는 쓰임이 있습니다. 이 쓰임을 따로 요구사항 커버리지라고도 부릅니다.
코드 커버리지는 기준이 언제나 코드입니다. 그리고 재는 실행이 테스트가 아니어도 됩니다. 퍼징의 실행도, 사람이 손으로 돌린 실행도 코드 커버리지로 잴 수 있습니다.
관련 항목
코드 커버리지를 세는 단위
라인 커버리지 · 구문 커버리지 · 분기 커버리지 · 함수 커버리지 · 조건 커버리지 · 경로 커버리지
코드 커버리지를 재는 도구와 기법
계측 · 커버리지 보고서 · JaCoCo · coverage.py · pytest-cov · pytest · Istanbul · gcov · 바이트코드 · 인터프리터 · 오버헤드
코드 커버리지를 신호로 쓰는 검사 방법
퍼징 · 커버리지 유도 퍼징 · 새니타이저 · 테스트 영향 분석
코드 커버리지를 재는 테스트 종류
단위 테스트 · 통합 테스트 · 회귀 테스트 · 테스트 스위트 · 인수 테스트
코드 커버리지가 못 보는 결함을 잡는 방법
단언 · 뮤테이션 테스트 · 속성 기반 테스트 · 코드 리뷰
코드 커버리지 수치를 합격선으로 쓰는 개발 단계
지속적 통합 · 품질 게이트 · 풀 리퀘스트 · 배포 파이프라인
코드 커버리지와 나란히 쓰는 품질 지표와 해석 원칙
테스트 커버리지 · 요구사항 커버리지 · 순환 복잡도 · 소프트웨어 지표 · 허영 지표 · 굿하트의 법칙
코드 커버리지가 속하는 상위 분류
다른 이름: code coverage