본문 바로가기

컴퓨터/python

테스트 커버리지 100% with 파이썬

얼마전에 Toss에서 진행하는 Slash21에서 인상적인 세션이 있었다.

영상을 보고 나니 테스트 커버리지를 100%를 유지하는 것이 상당히 이점이 많아 보여 이번에 새로 진행하는 프로젝트에 적용하기로 했다. (다만 실제로 100%를 실제로 적용하는 것은 꽤 요원한 일일 듯 하다.)

 

이번 포스트는 파이썬 프로젝트를 진행할 때 테스트 커버리지 100%를 달성(가능하도록)하기 위해 어떤 테스트 관련 설정을 했는지 공유하기로 한다.

 

테스트 도구 : pytest

python에도 다양한 테스트 도구들이 있지만, 검색으로 좀 찾아보니 대부분이 pytest를 사용하는 분위기다.

pytest의 장점은 fixture를 통해 테스트 시 반복적인 작업을 줄여주는 등의 장점이 있다.

커버리지 측정 : pytest-cov

커버리지 측정은 coverage 와 pytest-cov 둘로 좁혀지는데, 둘다 써 보니 큰 차이는 안나지만 pytest를 쓰다 보니 거기에 맞는 pytest-cov를 쓰기로 했다. 다만 부득이한 이유로 coverage의 설정파일인 .coveragerc를 pytest 시 config로 설정해야 하는데, 이는 후술할 것이다.

 

pytest 설정 파일

1. pytest.ini : 기본적인 pytest 구성 파일이다. 내 경우는 아래와 같이 구성했다.

#테스트 시 장고 설정
DJANGO_SETTINGS_MODULE = db.settings
# -v : 테스트 결과를 콘솔에 띄워줌 
# --cov-config.coveragerc : 커버리지 시 참고할 설정 파일 (테스트 커버리지에서 제외할 부분을 설정(후술))
# --cov=. : 테스트받을 파일들을 설정한다.
# --cov-report=html : report를 html 형태로 export하도록 설정. 기본 설정시 htmlcov 파일에 저장된다.
# --cov-fail-under 100 : 목표 커버리지를 100으로 설정하고, 테스트 결과 그에 미달할 경우 실패했다고 알려주는 옵션. 100% 달성 후에는 달성 실패 시 merge/배포가 안되도록 막을 것이다.
# tests/ : 테스트할 테스트파일을 지정한다. pytest는 파일명이 test로 시작해야 테스트파일로 인지한다.
addopts = -v --cov-config=.coveragerc --cov=. --cov-report=html --cov-fail-under 100 tests/

위와 같이 설정한 상태에서, pytest를 하면 작성했던 테스트가 실행되고 리포트 파일이 htmlcov 폴더 안에 저장된다. 한번 살펴보자.

? 나 모든 함수에 테스트 케이스를 작성했는데?

뭔가 이상하다. 나는 분명 해당 파일안의 모든 함수에 대해 테스트 함수를 작성했는데, 테스트 커버리지가 80%밖에 안된다.

스크롤을 아래로 내려보니, 내가 커버하지 못한 라인이 어느 라인인지 붉은색으로 표시해주고 있었다. 커버하지 못한 영역(missing)의 유형은 2가지였다.

1. Exception 처리

이 부분은 인정

이 부분은 내가 테스트를 작성할 때 happy path(정상작동) 부분만 생각해서 테스트를 하다보니, 예외처리하는 부분을 빠트려서 miss난 라인이다. 이 부분은 예외처리를 발생시키는 테스트를 작성해서 커버하면 된다.

2. pass

이 부분은 노인정

? pass 는 함수를 정의할 때 디스크립션 부분에 적을 게 없을 때 없다고 알려주는 커맨드인데, 이 부분을 커버하지 못했다고 나온다. 사실 무시해도 되는 부분이지만, 그렇게 할 경우 나의 목표인 100% 커버리지와는 영영 이별하게 된다. 특히 추상메소드를 쓸 경우 pass를 많이 사용할 수밖에 없다.

 

...pass를 어떻게 커버하지?

pass를 missing에서 제외하기 위해 여러 방법을 찾았지만, 마지막으로 적용한 방법만 여기서 공유하기로 한다.

https://github.com/nedbat/coveragepy/issues/1131

 

Ignore ABC abstract methods · Issue #1131 · nedbat/coveragepy

Is your feature request related to a problem? Please describe. Sometimes you want to create an abstract class with abstract methods that will be overridden by other subclasses. For example: import ...

github.com

#.coveragerc
[report]
exclude_lines =
    @abstractmethod
    @abc.abstractmethod

pytest는 아니고 coveragepy github 에 올라온 내용이긴 하지만, 위에서 적은 것처럼 config파일로 coveragerc를 활용하면 pass를 TEST 시 exclude 처리할 수 있다. 추상메소드 부분만 골라서 exclude하는 방법인데, 해당 config를 적용해서 pytest를 할 경우 missing난 부분이 아래와 같이 excluded 처리된다.

회색은 excluded 처리된 것이다.
pass를 주석처리했더니 커버리지율이 올랐다.

이렇게 진행할 경우 이제 예외처리에 대한 테스트 케이스만 작성하면 파일 하나에 대한 테스트 커버리지 100%를 달성할 수 있다. 희희.

현재의 커버리지는 미약하지만 끝은 100%가 되기를...