pytest 도입으로 자동매매 봇 테스트하기 | 토스 개발기 #1-3

얼마 전 재시도 데코레이터를 통합하는 작업을 하다가, 예전에 분명히 고쳤던 KRW/USD 통화 분리 로직이 코드에서 조용히 사라져 있는 걸 발견했습니다. 다행히 배포 전에 잡았지만, “고친 버그가 아무 알림 없이 다시 살아날 수 있다”는 사실 자체가 소름 돋는 경험이었습니다.

그날 바로 로드맵에 적어두기만 하고 미뤄뒀던 pytest 도입을 진행하기로 했습니다. 이번 글은 그 과정과, 테스트를 짜면서 발견한 (버그는 아니지만) 흥미로운 기존 동작 몇 가지를 기록한 내용입니다.

토스 자동매매 개발기 시리즈 (Phase 1/전체 5편)

← 이전 글: Phase 1-2 | 다음 글: 발행 후 추가 예정

왜 지금 테스트를 도입했나

사실 pytest는 로드맵에 처음부터 있었습니다. 다만 “일단 돌아가는 걸 먼저 만들고 나중에”라는 흔한 핑계로 계속 미뤄지고 있었죠. 그런데 재시도 로직 리팩터링 중에 통화 분리 로직이 사라진 걸 발견한 순간, 우선순위가 바로 바뀌었습니다. 사람이 코드 리뷰로 매번 잡아낼 수는 없으니, 기계가 대신 잡아주는 안전망이 필요했습니다.

테스트 대상 선정

원래 로드맵에는 get_display_width, fit_to_width 같은 콘솔 출력 관련 순수 함수만 적혀 있었습니다. 하지만 이번엔 범위를 넓혔습니다.

  • safe_int / safe_float / center_korean — 자주 쓰이는 순수 유틸 함수
  • extract_amounts_by_currency — 얼마 전 고쳤던 USD 누락 버그의 핵심 로직. 이건 반드시 회귀 방지 테스트로 고정해야 했습니다.
  • retry_on_network_error 데코레이터 — 성공/실패/부분실패/백오프 증가/예외 전파까지 케이스별로 검증
  • DRY_RUN, TARGET_BUY_PRICE 같은 안전장치 값 — 실수로 이 값이 바뀌면 테스트가 즉시 실패하도록 고정

안전장치 값을 테스트로 고정한다는 것

개인적으로 가장 마음에 든 부분은 DRY_RUNTARGET_BUY_PRICE 같은 안전장치 값 자체를 테스트로 박제한 것입니다. 예시로 단순화하면 이런 식입니다.

def test_dry_run_is_true_by_default():
    # 실수로 DRY_RUN이 False로 바뀌면 이 테스트가 즉시 실패한다
    assert config.DRY_RUN is True

def test_target_buy_price_masked_example():
    # 실제 값이 아닌 가상 종목/가격 예시
    assert config.TARGET_BUY_PRICE["005930"] == 70000

코드 로직이 아니라 “설정값 자체”를 테스트한다는 게 처음엔 좀 낯설었는데, 막상 넣어보니 실수로 안전장치를 해제한 채 배포하는 최악의 시나리오를 막아주는 가장 저렴한 보험이었습니다.

회귀 방지 테스트: extract_amounts_by_currency

이 함수는 얼마 전 실제로 사고가 났던 부분이라 가장 신경 써서 테스트를 짰습니다. 핵심만 남기면 대략 이런 구조입니다.

def test_extract_amounts_separates_krw_and_usd():
    raw_text = "보유자산: 1,000,000원 / $500.00"
    result = extract_amounts_by_currency(raw_text)
    assert result["KRW"] == 1000000
    assert result["USD"] == 500.00

이 테스트 하나가, 다음번에 누군가(미래의 나 포함) 이 함수 근처 코드를 건드리다가 통화 분리 로직을 실수로 지워도 즉시 알려주는 역할을 합니다.

버그는 아니지만 재미있었던 발견 두 가지

테스트를 짜다 보면 “어? 이게 왜 이렇게 되지”하고 멈칫하는 순간이 종종 생깁니다. 이번엔 두 가지가 그랬는데, 둘 다 실제로는 버그가 아니라 기존 설계 의도대로 동작하는 것으로 확인됐습니다.

  • safe_int("1234.0") 같이 소수점이 포함된 문자열은 변환에 실패해 기본값 0을 반환합니다. 처음엔 “이거 버그 아닌가” 싶었는데, 실제 API가 내려주는 원화 금액은 항상 정수 문자열이라 실사용에는 영향이 없었습니다.
  • fit_to_width가 한글 문자열을 말줄임 처리할 때, 너비를 정확히 맞추려고 끝에 공백이 붙는 경우가 있어서 항상 말줄임표(…)로 끝나지는 않습니다. 이것도 확인해보니 원래 설계 의도대로였습니다.

두 경우 모두 코드를 고치는 대신, 테스트가 실제 동작을 정확하게 기술하도록 맞췄습니다. “테스트가 원하는 동작”과 “코드의 실제 동작”이 다를 때, 무조건 코드를 테스트에 맞추기보다 먼저 의도를 확인하는 게 순서라는 걸 다시 한번 느꼈습니다.

사소하지만 발목 잡았던 것: Windows PATH

pip install pytest까지는 순조로웠는데, 정작 pytest -v를 실행하니 “‘pytest’ 용어가 인식되지 않습니다” 라는 에러가 떴습니다. pip로 설치된 실행 파일 경로가 Windows PATH에 등록되지 않은, 꽤 흔한 케이스였습니다.

해결은 간단했습니다. python -m pytest -v로 실행하니 바로 정상 동작했고, 이후로는 이 명령을 이 프로젝트의 표준 실행 방식으로 채택했습니다.

결과와 다음 단계

최종적으로 test_config.py(3개), test_toss_common.py(5개), test_toss_portfolio.py(25개), 총 33개 테스트가 전부 통과했습니다. 로컬 실행 폴더와 프로젝트 저장소 양쪽에 반영해서 재실행까지 확인했습니다.

Phase 1에서 남은 마지막 항목은 로그 파일이 무한정 커지는 문제를 RotatingFileHandler로 해결하는 것입니다. 이걸 끝내면 Phase 1의 안정성 기반 작업이 마무리되고, 다음은 전략 모듈화(Phase 2)로 넘어갈 예정입니다.

“pytest 도입으로 자동매매 봇 테스트하기 | 토스 개발기 #1-3”에 대한 2개의 생각

댓글 남기기