학습 기록

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로 파싱 함수만 따로 시험하는 방법을 정리했습니다.

핵심 정리

  1. assert 조건, "메시지" 는 조건이 거짓일 때만 에러를 낸다
  2. 응답 HTML을 파일로 한 번만 저장해두면 사이트에 접속하지 않고 몇 번이든 시험할 수 있다
  3. 함수가 문자열만 받게 짜여 있으면 시험하기가 쉬워진다
  4. 한 번 겪은 실수는 검사로 박아두면 같은 자리에서 다시 안 넘어진다
  5. 검사는 너무 빡빡하게 잡지 말고, 실패 메시지에 실제 값을 넣어둘 것

이제 할 수 있는 것들

  • 사이트에 요청을 보내지 않고 파싱 코드 확인하기
  • 빈 페이지, 값이 없는 항목 같은 상황을 문자열로 흉내내서 시험하기
  • 코드를 고친 뒤 python test_parser.py 한 줄로 안전 여부 확인하기
  • 예전에 겪은 버그를 검사 목록에 남겨두기

직장인 Python 공부를 하면서 스물여섯 번째 글인데, 이번 회차 이후로 코드 고치는 게 확실히 덜 무서워졌습니다. 업무 자동화 스크립트는 한 번 만들고 끝이 아니라 계속 손대게 되는데, 고쳐도 안 망가진다는 걸 확인할 방법이 있으니 손대기가 편해졌습니다.

다음 포스팅에서는 지금 손으로 부르고 있는 검사들을 pytest로 옮겨보겠습니다. if __name__ == "__main__": 밑에 함수 이름을 하나씩 적는 게 슬슬 귀찮아졌거든요. 설치부터 pytest 한 단어로 전부 실행되는 것까지, 그리고 실패했을 때 화면을 읽는 법까지 정리해볼 생각입니다.