학습 기록
Python 공부 기록 #27 - pytest로 검사 한 번에 돌리고 실패 화면 읽기
0. 목차
- 인트로: 함수 이름 적는 게 슬슬 귀찮아졌습니다
- 손으로 부르는 방식의 한계
- pytest 설치하고 한 단어로 돌려보기
- 1단계: 파일 이름과 함수 이름 규칙 맞추기
- 2단계: 실패 화면 읽는 법
- 3단계: 같은 준비 작업을 fixture로 빼기
- 삽질했던 부분들
- 아웃트로: 검사가 늘어나도 무섭지 않게
1. 인트로: 함수 이름 적는 게 슬슬 귀찮아졌습니다
지난 글에서 assert로 파싱 함수만 따로 시험하는 방법을 정리했습니다. 그 뒤로 검사가 계속 늘었습니다.
문제는 파일 맨 아래였습니다.
if __name__ == "__main__":
test_항목_개수()
test_제목_안비었는지()
test_빈_HTML도_안죽는지()
검사 함수를 새로 만들 때마다 여기에 이름을 한 줄씩 또 적어야 했습니다. 8월 11일에는 검사를 만들어놓고 아래에 적는 걸 깜빡했습니다. “전부 통과”가 떴는데 알고 보니 그 검사는 실행조차 안 된 상태였습니다.
통과했다는 말을 못 믿게 되는 순간, 검사를 만든 의미가 없어집니다. 찾아보니까 이런 걸 대신 해주는 도구가 pytest였습니다.
2. 손으로 부르는 방식의 한계
가장 큰 문제는 방금 겪은 그거였습니다. 사람이 적는 목록이라 빠뜨릴 수 있다는 것.
두 번째는 첫 실패에서 멈춘다는 점이었습니다. 검사 세 개 중 첫 번째가 걸리면 거기서 프로그램이 끝나버립니다. 나머지 두 개도 문제가 있는지는 첫 번째를 고친 뒤에야 알 수 있었습니다.
세 번째는 실패 메시지가 불친절했습니다. AssertionError와 줄 번호만 나오니까, 어떤 값이 들어와서 걸렸는지 보려면 print()를 다시 심어야 했습니다.
세 가지 다 pytest가 알아서 해결해주는 부분이었습니다.
3. pytest 설치하고 한 단어로 돌려보기
설치는 한 줄입니다.
pip install pytest
그리고 test_parser.py가 있는 폴더에서 이렇게만 칩니다.
pytest
if __name__ == "__main__": 아래 부분은 지웠습니다. 이제 필요 없습니다. pytest가 폴더를 뒤져서 검사 함수를 알아서 찾아 전부 돌려줍니다.
collected 3 items
test_parser.py ... [100%]
3 passed in 0.14s
점 세 개가 검사 세 개입니다. 이거 되겠는데? 싶었던 게 이 화면이었습니다. 실패하면 점 대신 F가 찍힙니다.
4. 1단계: 파일 이름과 함수 이름 규칙 맞추기
pytest가 검사를 어떻게 찾는지 궁금해서 찾아봤습니다. 규칙은 이름뿐이었습니다.
- 파일 이름이
test_로 시작하거나_test.py로 끝날 것 - 함수 이름이
test_로 시작할 것
test_parser.py 안의 test_항목_개수()는 두 조건을 다 만족하니까 그냥 잡혔습니다. 지난 글에서 우연히 맞춰놨던 셈입니다.
반대로 load_sample()은 test_로 시작하지 않아서 검사로 취급되지 않습니다. 도우미 함수는 이름만 다르게 두면 알아서 빠집니다.
5. 2단계: 실패 화면 읽는 법
일부러 틀린 검사를 하나 넣고 돌려봤습니다.
def test_항목_개수():
items = parse_items(load_sample())
assert len(items) == 20
사이트가 25건을 주도록 바뀐 상황이라 이건 실패합니다.
E assert 25 == 20
E + where 25 = len([{'title': '...'}, ...])
test_parser.py:9: AssertionError
assert 25 == 20. 메시지를 따로 안 붙였는데도 실제 값 25가 그대로 보입니다. pytest가 assert 문을 뜯어서 양쪽 값을 대신 찍어주는 겁니다.
여기서 좀 놀랐습니다. 지난 글에서 실패 메시지에 값을 넣으려고 f-string을 꼬박꼬박 붙였는데, 그 수고를 상당 부분 덜어줍니다. 물론 왜 20이어야 하는지 같은 설명은 여전히 직접 적는 게 낫습니다.
그리고 검사 하나가 실패해도 나머지는 끝까지 돕니다. 마지막에 1 failed, 2 passed처럼 한 줄로 정리해줍니다.
6. 3단계: 같은 준비 작업을 fixture로 빼기
검사마다 load_sample()을 부르는 줄이 계속 반복됐습니다. 이걸 정리하는 방법이 fixture였습니다.
import pytest
from crawler import parse_items
@pytest.fixture
def html():
with open("sample.html", encoding="utf-8") as f:
return f.read()
def test_항목_개수(html):
assert len(parse_items(html)) > 0
def test_제목_안비었는지(html):
for item in parse_items(html):
assert item["title"] != "", f"제목이 빈 항목: {item}"
@pytest.fixture를 붙인 함수는 준비 담당이 됩니다. 검사 함수 괄호 안에 그 이름을 적어두면, pytest가 먼저 실행해서 결과를 넣어줍니다.
처음엔 html이라는 값이 어디서 튀어나오는 건지 이해가 안 됐습니다. 이름이 곧 연결 고리라는 걸 알고 나서야 감이 왔습니다. 함수 이름과 매개변수 이름이 같으면 붙는다, 이게 전부였습니다.
7. 삽질했던 부분들
pip install pytest를 했는데 pytest 명령이 없다고 나왔습니다. 가상환경을 켜지 않은 채로 설치했던 게 원인이었습니다. python -m pytest로 부르면 지금 쓰는 Python 기준으로 실행되니까 이 방식이 더 안전하더군요.
검사 함수에 return을 쓰다가 경고를 봤습니다. 조건이 맞으면 True를 돌려주게 짰는데, pytest는 반환값을 보지 않습니다. assert가 걸리는지 아닌지만 봅니다. 반환값으로 통과 여부를 알린다고 착각하면 검사가 항상 통과하는 상태가 됩니다.
sample.html을 못 찾는 문제는 여전했습니다. 지난 글에서 넘어갔던 그 문제인데, 폴더 밖에서 pytest를 치면 또 걸립니다. 파일 경로를 실행 위치가 아니라 파일 위치 기준으로 잡는 방법이 있다는 것까지는 봤는데, 아직 제대로 이해 못 했습니다.
ChatGPT한테 fixture 예제를 달라고 했더니 데이터베이스 연결을 만드는 코드를 줬습니다. 제 상황과 너무 멀어서 “HTML 파일 읽는 경우로 바꿔서”라고 다시 물어봤습니다. 예제가 내 상황이랑 비슷해야 이해가 붙습니다.
8. 아웃트로: 검사가 늘어나도 무섭지 않게
오늘은 손으로 부르던 검사들을 pytest로 옮겨봤습니다.
핵심 정리
pip install pytest후 폴더에서pytest한 단어면 전부 실행된다- 파일 이름과 함수 이름이
test_로 시작하면 자동으로 찾아준다 - 실패해도 멈추지 않고 끝까지 돌린 뒤 통과와 실패 개수를 정리해준다
assert양쪽 값을 알아서 보여주니 f-string을 매번 붙이지 않아도 된다- 반복되는 준비 작업은
@pytest.fixture로 빼고 이름으로 연결한다
이제 할 수 있는 것들
- 검사 함수를 만들기만 하면 실행 목록에 적지 않아도 돌아가게 하기
- 여러 검사의 실패를 한 번에 확인하고 우선순위 정하기
- 실패 화면에서 실제 값을 바로 읽고 원인 좁히기
- 샘플 파일 읽기 같은 준비 작업을 한 곳에 모으기
비전공자 Python 공부를 시작한 지 스물일곱 번째 글입니다. 업무 자동화 스크립트를 만들다 보면 코드보다 검사가 더 늘어나는 시점이 오는데, 그때부터는 검사를 관리하는 도구가 따로 필요하다는 걸 이번에 알았습니다.
다음 포스팅에서는 실제 사이트 대신 가짜 응답을 돌려주는 방법을 정리해보겠습니다. 지금은 파싱 함수만 시험하고 있는데, requests.get()을 부르는 부분은 여전히 손도 못 대고 있거든요. unittest.mock으로 요청 함수를 바꿔치기해서 200 응답과 500 에러를 마음대로 만들어보는 것까지 해볼 생각입니다.