coverage.py
고친 사람 github-actions[bot]
coverage.py 는 파이썬 프로그램을 돌리면서 어느 코드가 실행됐는지 기록해 줍니다. 테스트를 돌린 뒤 한 번도 실행되지 않은 코드를 찾을 때 씁니다. 결과는 파일마다 퍼센트와 빠진 줄 번호로 나옵니다.
쉽고 빠른 이해
coverage.py 는 테스트가 코드의 어디를 지나갔는지 알려 줍니다. 배송비 함수를 테스트 하나로 돌리면 「7줄 중 6줄 실행, 4번 줄이 빠짐」 같은 표가 나옵니다.
테스트가 통과해도 그 테스트가 어느 코드를 건드렸는지는 따로 보이지 않습니다. 이 도구가 없으면 한 번도 실행된 적 없는 코드를 사람이 눈으로 찾아야 합니다.
어떻게 도는가:
- 파이썬이 줄을 하나 실행할 때마다 불러 주는 함수를 걸어 둡니다
- 그 함수가 실행된 줄 번호를 모아 파일에 저장합니다
- 소스 코드를 읽어 실행될 수 있는 줄을 셉니다
- 그 목록을 모은 기록과 맞대 빠진 줄을 찾습니다
대가는 측정하는 동안 프로그램이 느려진다는 점입니다. 그리고 숫자가 높아도 테스트가 결과 값을 확인했는지는 알 수 없습니다.
상세
coverage.py 는 파이썬용 코드 커버리지 도구입니다. 코드 커버리지는 프로그램이 도는 동안 코드의 어디가 실행됐는지를 재는 일입니다. 테스트가 손대지 않은 코드를 찾아야 그 코드에 테스트를 더할 수 있습니다.
이 절은 먼저 coverage.py 가 실행을 어떻게 기록하는지 봅니다. 그다음 배송비 함수 하나와 테스트 하나로 명령을 돌려 결과를 읽습니다. 끝으로 설정 몇 가지와 이 도구가 맞는 곳을 적습니다.
줄마다 불리는 트레이스 함수
파이썬 인터프리터는 코드를 한 줄씩 실행합니다. 인터프리터에는 새 줄로 넘어갈 때마다 미리 등록해 둔 함수를 불러 주는 기능이 있습니다. 이 함수를 트레이스 함수라고 부릅니다. 트레이스 함수는 sys.settrace 로 등록합니다.
coverage.py 는 프로그램을 시작하기 전에 트레이스 함수를 등록합니다. 이 함수는 불릴 때마다 지금 어느 파일의 몇 번째 줄인지를 적어 둡니다. 프로그램이 끝나면 적어 둔 목록을 파일에 저장합니다.
이렇게 도는 프로그램에 기록용 장치를 붙이는 일을 계측이라고 합니다. coverage.py 는 소스 코드를 한 글자도 고치지 않습니다. 인터프리터가 불러 주는 함수만으로 기록을 얻습니다.
트레이스 함수는 줄마다 불리므로 측정하는 동안 프로그램이 느려집니다. 이 부담을 줄이려고 coverage.py 에는 기록하는 부분을 C 로 짠 확장 모듈이 들어 있습니다. 확장 모듈은 파이썬이 불러 쓰는, C 로 짜서 미리 컴파일한 모듈입니다.
실행 기록과 소스 분석 맞대기
기록만으로는 안 간 코드를 알 수 없습니다. 기록에는 실행된 줄만 남기 때문입니다. 그래서 coverage.py 는 소스 파일을 따로 읽어 실행될 수 있는 코드의 목록을 만듭니다.
이 목록을 세는 단위는 줄이 아니라 구문입니다. 구문(statement)은 fee = 0 이나 return fee 처럼 실행되는 코드 한 단위입니다. 빈 줄과 주석은 구문이 아니라서 세지 않습니다. 여러 줄에 걸쳐 쓴 호출 하나는 구문 하나로 셉니다.
두 목록을 맞대면 결과가 나옵니다. 실행될 수 있는 구문 가운데 기록에 없는 것이 안 간 구문입니다. 실행된 구문의 비율을 구문 커버리지라고 부릅니다. coverage.py 는 이 비율을 파일마다 퍼센트로 보여 줍니다.
기록하고 보고하는 명령
coverage.py 는 명령줄 도구입니다. pip 으로 coverage 패키지를 설치하면 coverage 명령이 생깁니다. 쓰는 순서는 기록하기와 보고하기 둘입니다.
pip install coverage
coverage run -m pytest # 돌리며 기록
coverage report -m # 표로 요약
coverage html # 웹 페이지로
coverage run 이 프로그램을 돌리며 기록합니다. -m pytest 는 pytest 를 모듈로 불러 테스트를 돌리라는 뜻입니다. python -m pytest 에서 python 대신 coverage run 을 쓴 꼴입니다.
기록은 현재 폴더의 .coverage 파일에 저장됩니다. 이 파일은 SQLite 데이터베이스입니다. 보고 명령들은 프로그램을 다시 돌리지 않고 이 파일만 읽습니다.
flowchart TD
A["coverage run · 테스트를 돌리며 기록"] --> D[".coverage · 데이터 파일"]
D --> R["coverage report · 표"]
D --> H["coverage html · 웹 페이지"]
D --> X["coverage xml · 다른 도구가 읽는 파일"]
기록은 한 번만 합니다. 보고는 여러 모양으로 뽑습니다.
coverage html 은 소스 코드에 색을 칠한 웹 페이지를 htmlcov 폴더에 만듭니다. 실행된 줄과 안 간 줄이 다른 색이라 빈 곳을 눈으로 찾을 수 있습니다. coverage xml 과 coverage json 은 다른 도구가 받아 읽을 파일을 만듭니다.
요약 표 읽기
작은 코드로 결과를 읽어 봅니다. 아래 함수는 주문 금액과 회원 여부를 받아 배송비를 돌려줍니다. shipping.py 파일의 1번 줄부터 7번 줄까지가 이 함수입니다.
def shipping_fee(amount, is_member):
fee = 3000
if amount >= 50000:
fee = 0
if is_member:
fee = fee // 2
return fee
테스트는 하나만 둡니다. 금액 10000 원을 내는 회원의 배송비를 확인합니다.
from shipping import shipping_fee
def test_member():
fee = shipping_fee(10000, True) # 1500
assert fee == 1500
coverage run --source=shipping -m pytest 로 돌린 뒤 coverage report -m 을 부릅니다. --source 는 측정할 코드를 shipping 모듈로 좁혀 테스트 파일이 표에 섞이지 않게 합니다. 표에서 이 파일의 줄은 이렇습니다.
Name Stmts Miss Cover Missing
-------------------------------------------
shipping.py 7 1 86% 4
칸마다 뜻이 다릅니다. 아래 표가 칸 이름과 이 예의 값을 짝지어 줍니다.
| 칸 | 뜻 | 이 예의 값 |
|---|---|---|
| Stmts | 실행될 수 있는 구문 수 | 7 |
| Miss | 한 번도 실행되지 않은 구문 수 | 1 |
| Cover | 실행된 구문의 비율 | 86% |
| Missing | 안 간 구문의 줄 번호 | 4 |
def 줄도 구문으로 셉니다. 모듈을 불러올 때 함수를 만드는 코드가 실행되기 때문입니다. 구문은 모두 일곱 개입니다.
금액 10000 은 50000 에 못 미치므로 4번 줄 fee = 0 은 실행되지 않았습니다. 일곱 개 중 여섯 개가 실행됐으므로 Cover 칸에 86% 가 찍힙니다. -m 을 빼면 Missing 칸이 빠진 표가 나옵니다.
분기까지 세는 --branch
요약 표는 4번 줄만 비었다고 알려 줍니다. 이 테스트가 놓친 경우는 하나 더 있습니다. 회원이 아닐 때입니다.
if is_member: 가 거짓이면 6번 줄을 건너뛰고 7번 줄로 갑니다. 이 길에는 자기 몫의 구문이 없습니다. 그래서 구문만 세면 이 길이 비어 있어도 드러나지 않습니다.
coverage run 에 --branch 를 붙이면 분기도 셉니다. 분기는 if 처럼 두 길로 갈라지는 줄에서 나가는 갈래 하나입니다. 이 함수에서 갈라지는 줄은 3번과 5번이라 분기는 넷입니다. 3→4, 3→5, 5→6, 5→7 입니다.
트레이스 함수는 줄 번호 하나 대신 「몇 번 줄에서 몇 번 줄로」 넘어갔는지를 짝으로 적습니다. 이렇게 세는 방식을 분기 커버리지라고 부릅니다.
flowchart TD
L3["3 · if amount >= 50000"] -. 안 감 .-> L4["4 · fee = 0"]
L3 -- 감 --> L5["5 · if is_member"]
L5 -- 감 --> L6["6 · fee = fee // 2"]
L5 -. 안 감 .-> L7["7 · return fee"]
그림에는 분기 넷만 그렸습니다. 실선이 이번 테스트가 간 분기, 점선이 안 간 분기입니다. 3번 줄의 두 갈래 중 하나, 5번 줄의 두 갈래 중 하나가 비었습니다. 4→5 나 6→7 처럼 갈라지지 않는 줄의 이음은 분기가 아니라서 뺐습니다.
분기를 켜면 Missing 칸에 5->7 이 더 붙습니다. 5번 줄에서 7번 줄로 곧장 가는 길을 한 번도 안 갔다는 뜻입니다. 3번 줄에서 4번 줄로 가는 길은 4번 줄이 이미 빠진 줄로 나오므로 따로 적지 않습니다.
퍼센트도 구문과 분기를 합쳐 다시 셉니다. 구문 일곱 개 중 여섯 개, 분기 네 개 중 두 개를 지났습니다. 합치면 열한 개 중 여덟 개라 Cover 칸은 73% 로 내려갑니다.
측정에서 빼는 코드
테스트로 지나기 어려운 코드도 있습니다. 디버깅할 때만 켜는 출력이나, 도달하면 안 되는 방어 코드가 그렇습니다. 이런 줄 끝에 # pragma: no cover 주석을 붙이면 coverage.py 가 그 줄을 세지 않습니다.
if DEBUG: # pragma: no cover
dump_state()
블록을 여는 줄에 붙이면 그 아래 블록까지 함께 빠집니다. 위 코드에서는 if 줄과 dump_state() 줄이 둘 다 세는 대상에서 빠집니다.
파일 단위로 빼려면 --omit 에 경로 패턴을 줍니다. 장고가 데이터베이스 표 구조를 바꾸려고 자동으로 만든 마이그레이션 파일은 --omit="*/migrations/*" 로 뺍니다. 사람이 테스트를 쓰지 않는 파일이라 세면 퍼센트만 내려갑니다.
설정 파일
명령마다 옵션을 붙이는 대신 설정 파일에 적어 둘 수 있습니다. .coveragerc 파일이나 pyproject.toml 의 [tool.coverage] 아래에 적습니다.
[tool.coverage.run]
branch = true
source = ["shipping"]
[tool.coverage.report]
show_missing = true
fail_under = 80
[tool.coverage.run] 은 기록할 때 읽는 설정입니다. [tool.coverage.report] 는 보고할 때 읽는 설정입니다. branch 는 --branch 를, source 는 --source 를, show_missing 은 -m 을 대신합니다.
합격선으로 쓰는 fail_under
fail_under 는 합격선입니다. 전체 퍼센트가 이 값보다 낮으면 coverage report 가 0 이 아닌 종료 코드로 끝납니다. 종료 코드는 명령이 성공했는지를 알리는 숫자입니다. 0 이 성공입니다.
이 값은 지속적 통합(CI, Continuous Integration)에서 쓸모가 있습니다. CI 는 코드를 올릴 때마다 빌드와 테스트를 자동으로 돌리는 체계입니다. CI 는 명령의 종료 코드로 성공과 실패를 가르므로, 커버리지를 떨어뜨린 변경을 이 값 하나로 막을 수 있습니다.
퍼센트는 지나간 코드의 비율일 뿐입니다. 지나간 코드가 맞는 값을 냈는지는 재지 않습니다. 앞의 테스트에서 assert 줄을 지워도 표는 똑같이 86% 를 보여 줍니다.
여러 번 돌린 기록 합치기
테스트를 여러 프로세스로 나눠 돌리면 기록도 여럿 생깁니다. 모두 같은 .coverage 파일에 쓰면 서로 덮어씁니다. 그래서 coverage run -p 로 돌리면 파일 이름 뒤에 프로세스마다 다른 꼬리가 붙습니다.
coverage combine 은 이 파일들을 .coverage 하나로 합칩니다. 합친 뒤에는 보고 명령을 평소처럼 부릅니다. 단위 테스트와 통합 테스트를 따로 돌린 기록을 합칠 때도 같은 방법을 씁니다.
pytest 에서 부르는 pytest-cov
pytest 를 쓰는 프로젝트는 pytest-cov 플러그인으로 coverage.py 를 부르기도 합니다. 플러그인은 pytest 에 기능을 덧붙이는 확장입니다. pytest --cov=shipping 처럼 옵션 하나로 기록과 보고를 함께 합니다.
측정은 두 방법 모두 coverage.py 가 합니다. pytest-cov 는 테스트가 도는 동안 coverage.py 를 켜고 끄는 일을 맡습니다. 앞의 설정 파일은 두 방법에 똑같이 적용됩니다.
coverage.py 가 맞는 곳과 안 맞는 곳
coverage.py 는 파이썬 코드만 잽니다. Java 에는 JaCoCo, 자바스크립트에는 Istanbul 처럼 언어마다 따로 도구가 있습니다.
파이썬 프로그램이라도 C 로 짠 확장 모듈 안은 재지 못합니다. 트레이스 함수는 파이썬 코드의 줄이 실행될 때만 불리기 때문입니다. 확장 모듈을 부르는 파이썬 줄까지만 기록에 남습니다.
측정에는 늘 느려짐이 따라붙습니다. 그래서 대개 테스트를 돌릴 때만 켭니다. 결과는 테스트가 비워 둔 코드를 찾는 데 씁니다. 테스트가 값을 제대로 확인하는지는 뮤테이션 테스트 같은 다른 방법으로 봅니다.
관련 항목
coverage.py 가 재는 커버리지 단위
코드 커버리지 · 구문 커버리지 · 라인 커버리지 · 분기 커버리지 · 테스트 커버리지
coverage.py 를 불러 쓰는 테스트 도구
pytest · pytest-cov · pytest-xdist · unittest · tox · nox
coverage.py 가 기록을 얻는 인터프리터 기능
계측 · sys.settrace · sys.monitoring · 트레이스 함수 · CPython · 인터프리터 · 확장 모듈
coverage.py 결과를 받아 쓰는 체계
지속적 통합 · 커버리지 보고서 · Codecov · GitHub Actions · 품질 게이트 · 종료 코드
다른 언어에서 같은 역할을 하는 커버리지 도구
JaCoCo · Istanbul · gcov · llvm-cov
coverage.py 가 못 보는 결함을 잡는 검사 방법
뮤테이션 테스트 · 속성 기반 테스트 · Hypothesis · 퍼징
coverage.py 가 속하는 상위 분류
다른 이름: coverage · Coverage.py