사전 coverage.py
구현체

coverage.py

gabury1고친 사람 github-actions[bot]

coverage.py 는 파이썬 프로그램을 돌리면서 어느 코드가 실행됐는지 기록해 줍니다. 테스트를 돌린 뒤 한 번도 실행되지 않은 코드를 찾을 때 씁니다. 결과는 파일마다 퍼센트와 빠진 줄 번호로 나옵니다.

쉽고 빠른 이해

coverage.py 는 테스트가 코드의 어디를 지나갔는지 알려 줍니다. 배송비 함수를 테스트 하나로 돌리면 「7줄 중 6줄 실행, 4번 줄이 빠짐」 같은 표가 나옵니다.

테스트가 통과해도 그 테스트가 어느 코드를 건드렸는지는 따로 보이지 않습니다. 이 도구가 없으면 한 번도 실행된 적 없는 코드를 사람이 눈으로 찾아야 합니다.

어떻게 도는가:

  1. 파이썬이 줄을 하나 실행할 때마다 불러 주는 함수를 걸어 둡니다
  2. 그 함수가 실행된 줄 번호를 모아 파일에 저장합니다
  3. 소스 코드를 읽어 실행될 수 있는 줄을 셉니다
  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번 줄까지가 이 함수입니다.

Python
def shipping_fee(amount, is_member):
    fee = 3000
    if amount >= 50000:
        fee = 0
    if is_member:
        fee = fee // 2
    return fee

테스트는 하나만 둡니다. 금액 10000 원을 내는 회원의 배송비를 확인합니다.

Python
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 가 그 줄을 세지 않습니다.

Python
if DEBUG:  # pragma: no cover
    dump_state()

블록을 여는 줄에 붙이면 그 아래 블록까지 함께 빠집니다. 위 코드에서는 if 줄과 dump_state() 줄이 둘 다 세는 대상에서 빠집니다.

파일 단위로 빼려면 --omit 에 경로 패턴을 줍니다. 장고가 데이터베이스 표 구조를 바꾸려고 자동으로 만든 마이그레이션 파일은 --omit="*/migrations/*" 로 뺍니다. 사람이 테스트를 쓰지 않는 파일이라 세면 퍼센트만 내려갑니다.

설정 파일

명령마다 옵션을 붙이는 대신 설정 파일에 적어 둘 수 있습니다. .coveragerc 파일이나 pyproject.toml 의 [tool.coverage] 아래에 적습니다.

TOML
[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 가 속하는 상위 분류

Python · 테스트 자동화 · QA와 테스트 · 단위 테스트 · 통합 테스트

다른 이름: coverage · Coverage.py