pytest 사용기 - fixture, scope


파이썬에서 제공하는 테스팅 모듈로는 unittest, pytest가 유명한데, 둘 중 어느걸 사용해 볼까 하다가 pytest가 간단해 보여서 pytest로 작업을 진행했습니다. 빠른 진행을 위해서는 테스팅 모듈이 간편한게 좋을 것 같다는 생각이 들었기 때문입니다. pytest 어떻게 사용했는지 살펴보겠습니다.

설치

pytest는 pip 로 설치하며 기초 설명은 공식 홈페이지에서 확인 가능합니다.


1
pip install pytest

프로젝트 구조


테스트 코드를 작성한 프로젝트의 구조는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.
├── logparser
│   ├── __init__.py
│   ├── models.py
│   └── parser.py
├── logs
│   ├── elb1.log.gz
│   ├── elb2.log.gz
├── pytest.ini
└── test
   ├── __init__.py
   ├── conftest.py
   ├── logs/
   └── test_parser.py

pytest를 위해 작성한 파일은 pytest.ini와 test폴더 하위의 파일들입니다. 먼저, pytest.ini에는 테스트에 사용할 파일과 테스트 옵션을 추가합니다. 일반적으로 test_*.py 또는 *_test.py를 테스트 파일로 인식하지만 직접 지정해줄 수도 있습니다. 파일이 여러 개인 경우 스페이스로 구분합니다.

1
2
[pytest]
python_files = test_parser.py test_*.py

테스트는 pytest 명령어를 사용해 실행합니다. 실행하는 경로에 상관없이 pytest용 파일을 찾아 실행합니다. q옵션을 사용하면 로그를 간략하게 볼 수 있습니다.

1
2
3
(venv) yejinui-MacBook-Pro:logparser yejin$ pytest -q
..... [100%]
5 passed in 31.78s

테스트 코드 작성

테스트 함수에는 아래와 같이 함수명에 접두사로 test_ 를 붙여야 합니다. 접두사가 없으면 그 함수는 실행하지 않고 넘어갑니다. count 함수와 sequence 함수를 테스트 하기 위해 각각 test_count, test_sequnce함수를 만들었습니다. test_count는 로그 수가 맞는지를 확인하고 test_sequence는 ‘type’ 별 집계가 일치하는지 확인합니다.

1
2
3
4
5
6
7
8
9
10
11
12
#test_parser.py
def test_count():
logs = log_parser(get_test_log)
assert count(logs) == 87060

def test_sequence():
logs = log_parser(get_test_log)
assert sequence(logs, 'type', reverse=True) == [
('http', 82719),
('h2', 3398),
('https', 943)
]

두 함수에서는 공통적으로 log_parser()함수를 사용해 로그를 읽어옵니다. 예시 이외에도 로그를 읽어 오는 테스트가 더 있어 클래스를 정의하는 방식으로 변경하였습니다. self.logs를 사용하는 방식으로 코드를 수정했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class TestParserClass:
def __init__(self, get_test_log):
self.logs = list(log_parser(get_test_log))

def test_count(self):
assert count(self.logs) == 87060

def test_sequence(self):
assert sequence(self.logs, 'type', reverse=True) == [
('http', 82719),
('h2', 3398),
('https', 943)
]

그런데 에러가 발생했습니다. 클래스에 __init__를 정의한 것이 문제가 되었습니다. 공식 홈페이지를 확인해보니 __init__를 정의하는 경우 클래스가 인스턴스화 되지 않는다고 하네요.

1
2
3
4
5
6
7
8
(venv) yejinui-MacBook-Pro:logparser yejin$ pytest -q
=============================================================================== warnings summary ================================================================================
test/test_parser.py:9
/Users/yejin/Sites/project/logparser/test/test_parser.py:9: PytestCollectionWarning: cannot collect test class 'TestParserClass' because it has a __init__ constructor (from: test/test_parser.py)
class TestParserClass:

-- Docs: https://docs.pytest.org/en/stable/warnings.html
1 passed, 1 warning in 0.03s

그래서 fixture를 사용했습니다. fixture란, 테스트 시 사용하는 함수들을 미리 정의하는 기능입니다.

fixture함수는 test/conftest.py에 작성합니다.

1
2
3
4
5
#test/conftest.py

@pytest.fixture(scope="module")
def test_log_parse():
return list(log_parser(get_log('./logs/')))

test_ 함수에서 사용할 때에는 따로 conftest를 임포트할 필요없이 매개변수로 지정하기만 하면됩니다.

1
2
3
4
5
6
7
8
9
10
#test_parser.py
def test_count(test_log_parse):
assert count(test_log_parse) == 87060

def test_sequence(test_log_parse):
assert sequence(test_log_parse, 'type', reverse=True) == [
('http', 82719),
('h2', 3398),
('https', 943)
]

굳이 fixture를 사용하지 않고 test_log_parse 함수를 만들어도 되는게 아닌가 생각할 수도 있습니다. 그렇게도 가능합니다. 그러나 깨끗한 테스트 코드를 위해 변하지 않는 기능은 한 곳에 넣어두고 자주 변경되며 확장할 기능을 만들도록 하는 것이 관리에 좋아 보입니다. 클래스 상속 개념처럼요🙂 그리고 무엇보다도 fixture를 사용하면 fixture가 제공하는 부가 옵션들을 사용할 수 있습니다. 여기서는 scope를 사용했습니다.

scope를 사용해 fixture를 모듈, 클래스, 세션 단위로 공유합니다. 다시 말해, test_ 함수를 실행할 때마다 fixture함수를 재호출하지 않고 사용할 수 있다는 것입니다. scope를 module 단위로 지정했더니, test_log_parse 한 번 호출 후 계속 재사용하기 때문에 아래와 같이 실행 시간이 단축됩니다.

1
2
3
4
5
6
7
8
9
//scope 사용
(venv) yejinui-MacBook-Pro:logparser yejin$ pytest -q
..... [100%]
5 passed in 34.24s

//scope 미사용
(venv) yejinui-MacBook-Pro:logparser yejin$ pytest -q
..... [100%]
5 passed in 131.13s (0:02:11)

Comments