학습 기록
Python 공부 기록 #26 - assert로 파싱 함수만 따로 시험해보기
0. 목차
- 인트로: 확인하려고 사이트를 또 긁었습니다
- 매번 실제 사이트로 확인하면 생기는 일
- assert가 하는 일은 딱 하나입니다
- 1단계: HTML을 한 번만 받아서 파일로 저장해두기
- 2단계: 저장한 HTML로 파싱 함수만 시험하기
- 3단계: 검사들을 test_parser.py 한 파일로 모으기
- 삽질했던 부분들
- 아웃트로: 겁내지 않고 코드를 고친다는 것
1. 인트로: 확인하려고 사이트를 또 긁었습니다
지난 글에서 220줄짜리 크롤러를 파일 다섯 개로 쪼갰습니다. 고칠 자리를 바로 찾게 된 건 좋았는데, 그 다음이 문제였습니다.
parse_items() 안에서 선택자 한 줄을 바꾸고 제대로 되는지 보려면 main.py를 통째로 돌려야 했습니다. 요청 보내고, 파싱하고, Excel 저장하고, Discord 알림까지 다 나간 뒤에야 결과를 볼 수 있었습니다.
한 줄 고치고 확인하는 데 20초씩 걸렸고, 그걸 스무 번쯤 반복했습니다.
8월 10일 밤에는 같은 사이트에 30분 동안 마흔 번 넘게 요청을 보내다가 응답이 느려지는 걸 봤습니다. 차단당하기 직전이었던 것 같습니다.
찾아보니까 이런 걸 테스트라고 부르더군요. 거창한 도구부터 배워야 하나 싶었는데, assert 한 단어면 시작할 수 있었습니다.
2. 매번 실제 사이트로 확인하면 생기는 일
일단 느립니다. 네트워크 왕복 시간이 매번 붙습니다. 그리고 내 코드를 확인하려고 남의 서버를 계속 두드리는 셈이라 민폐이기도 합니다.
제일 곤란한 건 결과가 흔들린다는 점이었습니다. 어제는 통과했는데 오늘 실패하면 내 코드가 문제인지 사이트가 바뀐 건지 구분이 안 됩니다.
확인하고 싶은 건 파싱 함수 하나인데 매번 프로그램 전체를 돌리고 있었습니다.
3. assert가 하는 일은 딱 하나입니다
assert는 “이건 참이어야 한다”고 선언하는 문장입니다.
assert 1 + 1 == 2 # 조용히 지나감
assert 1 + 1 == 3 # AssertionError 발생
참이면 아무 일도 없고, 거짓이면 그 자리에서 에러를 내며 멈춥니다. 뒤에 쉼표를 찍고 assert len(items) > 0, "항목이 하나도 안 뽑혔습니다"처럼 메시지를 붙일 수도 있습니다.
처음엔 print()로 찍어보는 거랑 뭐가 다른가 싶었습니다. 차이는 확실합니다. print()는 내가 눈으로 읽고 판단해야 하지만, assert는 틀렸을 때 알아서 소리를 지릅니다. 스무 개를 검사해도 조용하면 전부 통과했다는 뜻입니다.
4. 1단계: HTML을 한 번만 받아서 파일로 저장해두기
핵심은 간단했습니다. 사이트 응답을 딱 한 번만 받아서 파일로 저장해두고, 그 다음부터는 그 파일로 시험하는 겁니다.
import requests
url = "https://example.com/list"
html = requests.get(url, timeout=10).text
with open("sample.html", "w", encoding="utf-8") as f:
f.write(html)
이 코드는 한 번만 돌립니다. 이렇게 받아둔 파일을 샘플 데이터라고 부르더군요.
이제 sample.html은 절대 변하지 않습니다. 어제도 오늘도 같은 내용이니까 파싱 결과가 달라지면 원인은 무조건 내 코드입니다.
5. 2단계: 저장한 HTML로 파싱 함수만 시험하기
지난 글에서 crawler.py를 만들어두길 잘했습니다. parse_items()가 HTML 문자열만 받게 되어 있으니 그대로 부르면 됩니다.
from crawler import parse_items
with open("sample.html", encoding="utf-8") as f:
html = f.read()
items = parse_items(html)
assert len(items) == 20, f"20건이어야 하는데 {len(items)}건입니다"
assert items[0]["title"] != "", "첫 항목 제목이 비었습니다"
assert "," not in items[0]["price"], "가격에 쉼표가 남아 있습니다"
print("통과")
요청을 아예 안 보내니까 결과가 0.1초 만에 나옵니다. 20초가 0.1초가 됐습니다.
세 번째 줄은 제가 나중에 추가한 검사입니다. 가격을 숫자로 바꾸는 처리를 넣었는데 쉼표가 그대로 남아 있어서 Excel에서 계산이 안 됐던 적이 있거든요. 한 번 겪은 실수는 이렇게 assert로 박아두면 다시는 같은 데서 안 넘어집니다.
6. 3단계: 검사들을 test_parser.py 한 파일로 모으기
검사가 늘어나니까 지난 글에서 만든 폴더에 sample.html과 test_parser.py를 나란히 넣었습니다. test_parser.py 안은 이렇게 정리했습니다.
from crawler import parse_items
def load_sample():
with open("sample.html", encoding="utf-8") as f:
return f.read()
def test_항목_개수():
items = parse_items(load_sample())
assert len(items) == 20
def test_제목_안비었는지():
items = parse_items(load_sample())
for item in items:
assert item["title"] != "", f"제목이 빈 항목: {item}"
def test_빈_HTML도_안죽는지():
items = parse_items("<html></html>")
assert items == [], "빈 페이지면 빈 리스트를 줘야 합니다"
if __name__ == "__main__":
test_항목_개수()
test_제목_안비었는지()
test_빈_HTML도_안죽는지()
print("전부 통과")
마지막 검사가 개인적으로 제일 마음에 듭니다. 사이트가 점검 중이라 빈 페이지가 오는 상황을 실제로 만들 수는 없는데, 문자열만 넘겨주면 그 상황을 흉내낼 수 있습니다.
이제 crawler.py를 고칠 때마다 python test_parser.py를 돌립니다. “전부 통과”가 뜨면 안심하고 넘어갑니다.
7. 삽질했던 부분들
assert len(items) == 20으로 박아뒀는데 사이트가 한 페이지에 25건을 보여주도록 바뀌면서 계속 실패했습니다. 정확한 숫자로 못 박는 대신 assert len(items) > 0처럼 느슨하게 바꿨습니다. 검사가 너무 빡빡하면 멀쩡한 코드도 자꾸 걸리더군요.
test_parser.py를 다른 폴더에서 실행했더니 sample.html을 못 찾았습니다. 파일 경로가 실행 위치 기준이라 그렇습니다. 일단 폴더 안으로 들어가서 돌리는 걸로 넘어갔는데, 제대로 된 해결책은 더 공부해야 할 것 같습니다.
처음엔 메시지를 안 붙였더니 AssertionError 한 줄만 뜨고 끝이었습니다. 어느 줄인지는 알겠는데 무슨 값이 들어와서 걸린 건지가 안 보였습니다. 그 뒤로는 쉼표 뒤에 설명을 꼭 붙입니다. f"20건이어야 하는데 {len(items)}건입니다"처럼 실제 값을 넣어두면 원인이 바로 보입니다.
ChatGPT한테 검사 목록을 짜달라고 했더니 처음 보는 라이브러리를 쓰는 코드를 줬습니다. assert부터 이해하고 싶어서 “표준 문법만 써서 다시”라고 물어봤습니다. 모르는 도구를 붙이는 것보다 아는 문법으로 굴리는 게 먼저였습니다.
8. 아웃트로: 겁내지 않고 코드를 고친다는 것
오늘은 assert로 파싱 함수만 따로 시험하는 방법을 정리했습니다.
핵심 정리
assert 조건, "메시지"는 조건이 거짓일 때만 에러를 낸다- 응답 HTML을 파일로 한 번만 저장해두면 사이트에 접속하지 않고 몇 번이든 시험할 수 있다
- 함수가 문자열만 받게 짜여 있으면 시험하기가 쉬워진다
- 한 번 겪은 실수는 검사로 박아두면 같은 자리에서 다시 안 넘어진다
- 검사는 너무 빡빡하게 잡지 말고, 실패 메시지에 실제 값을 넣어둘 것
이제 할 수 있는 것들
- 사이트에 요청을 보내지 않고 파싱 코드 확인하기
- 빈 페이지, 값이 없는 항목 같은 상황을 문자열로 흉내내서 시험하기
- 코드를 고친 뒤
python test_parser.py한 줄로 안전 여부 확인하기 - 예전에 겪은 버그를 검사 목록에 남겨두기
직장인 Python 공부를 하면서 스물여섯 번째 글인데, 이번 회차 이후로 코드 고치는 게 확실히 덜 무서워졌습니다. 업무 자동화 스크립트는 한 번 만들고 끝이 아니라 계속 손대게 되는데, 고쳐도 안 망가진다는 걸 확인할 방법이 있으니 손대기가 편해졌습니다.
다음 포스팅에서는 지금 손으로 부르고 있는 검사들을 pytest로 옮겨보겠습니다. if __name__ == "__main__": 밑에 함수 이름을 하나씩 적는 게 슬슬 귀찮아졌거든요. 설치부터 pytest 한 단어로 전부 실행되는 것까지, 그리고 실패했을 때 화면을 읽는 법까지 정리해볼 생각입니다.