테스트 강화 및 코드 품질 개선

This commit is contained in:
2025-12-17 00:01:46 +09:00
parent 37a150bd0d
commit 00c57ddd32
51 changed files with 10670 additions and 217 deletions

View File

@@ -0,0 +1,328 @@
# src/tests/test_config_validation.py
"""
HIGH-002: 설정 검증 로직 테스트
config.py의 validate_config() 함수에 추가된 검증 로직을 테스트합니다:
1. Auto Trade 활성화 시 API 키 필수
2. 손절/익절 주기 논리 검증 (경고)
3. 스레드 수 범위 검증
4. 최소 주문 금액 검증
5. 매수 금액 검증
"""
import os
from unittest.mock import patch
from src.config import validate_config
class TestConfigValidation:
"""설정 검증 로직 테스트"""
def test_valid_config_minimal(self):
"""최소한의 필수 항목만 있는 유효한 설정"""
cfg = {
"buy_check_interval_minutes": 240,
"stop_loss_check_interval_minutes": 60,
"profit_taking_check_interval_minutes": 240,
"dry_run": True,
"auto_trade": {
"enabled": False,
"buy_enabled": False,
},
"confirm": {
"confirm_stop_loss": False,
},
"max_threads": 3,
}
is_valid, error = validate_config(cfg)
assert is_valid is True
assert error == ""
def test_missing_required_key(self):
"""필수 항목 누락 시 검증 실패"""
cfg = {
"buy_check_interval_minutes": 240,
# "stop_loss_check_interval_minutes": 60, # 누락
"profit_taking_check_interval_minutes": 240,
"dry_run": True,
"auto_trade": {},
}
is_valid, error = validate_config(cfg)
assert is_valid is False
assert "stop_loss_check_interval_minutes" in error
def test_invalid_interval_value(self):
"""잘못된 간격 값 (0 이하)"""
cfg = {
"buy_check_interval_minutes": 0, # 잘못된 값
"stop_loss_check_interval_minutes": 60,
"profit_taking_check_interval_minutes": 240,
"dry_run": True,
"auto_trade": {},
"confirm": {"confirm_stop_loss": False},
}
is_valid, error = validate_config(cfg)
assert is_valid is False
assert "buy_check_interval_minutes" in error
def test_auto_trade_without_api_keys(self):
"""HIGH-002-1: auto_trade 활성화 시 API 키 없으면 실패"""
cfg = {
"buy_check_interval_minutes": 240,
"stop_loss_check_interval_minutes": 60,
"profit_taking_check_interval_minutes": 240,
"dry_run": False,
"auto_trade": {
"enabled": True, # 활성화
"buy_enabled": True,
},
"confirm": {"confirm_stop_loss": False},
"max_threads": 3,
}
# API 키 없는 상태로 테스트
with patch.dict(os.environ, {}, clear=True):
is_valid, error = validate_config(cfg)
assert is_valid is False
assert "UPBIT_ACCESS_KEY" in error or "UPBIT_SECRET_KEY" in error
def test_auto_trade_with_api_keys(self):
"""HIGH-002-1: auto_trade 활성화 + API 키 있으면 성공"""
cfg = {
"buy_check_interval_minutes": 240,
"stop_loss_check_interval_minutes": 60,
"profit_taking_check_interval_minutes": 240,
"dry_run": False,
"auto_trade": {
"enabled": True,
"buy_enabled": True,
},
"confirm": {"confirm_stop_loss": False},
"max_threads": 3,
}
# API 키 있는 상태로 테스트
with patch.dict(os.environ, {"UPBIT_ACCESS_KEY": "test_key", "UPBIT_SECRET_KEY": "test_secret"}):
is_valid, error = validate_config(cfg)
assert is_valid is True
assert error == ""
def test_stop_loss_interval_greater_than_profit(self, caplog):
"""HIGH-002-2: 손절 주기 > 익절 주기 시 경고 로그"""
cfg = {
"buy_check_interval_minutes": 240,
"stop_loss_check_interval_minutes": 300, # 5시간
"profit_taking_check_interval_minutes": 60, # 1시간
"dry_run": True,
"auto_trade": {"enabled": False},
"confirm": {"confirm_stop_loss": False},
"max_threads": 3,
}
is_valid, error = validate_config(cfg)
assert is_valid is True # 검증은 통과 (경고만 출력)
# 경고 로그 확인
assert any("손절 주기" in record.message for record in caplog.records)
def test_max_threads_invalid_type(self):
"""HIGH-002-3: max_threads가 정수가 아니면 실패"""
cfg = {
"buy_check_interval_minutes": 240,
"stop_loss_check_interval_minutes": 60,
"profit_taking_check_interval_minutes": 240,
"dry_run": True,
"auto_trade": {"enabled": False},
"confirm": {"confirm_stop_loss": False},
"max_threads": "invalid", # 잘못된 타입
}
is_valid, error = validate_config(cfg)
assert is_valid is False
assert "max_threads" in error
def test_max_threads_too_high(self, caplog):
"""HIGH-002-3: max_threads > 10 시 경고"""
cfg = {
"buy_check_interval_minutes": 240,
"stop_loss_check_interval_minutes": 60,
"profit_taking_check_interval_minutes": 240,
"dry_run": True,
"auto_trade": {"enabled": False},
"confirm": {"confirm_stop_loss": False},
"max_threads": 15, # 과도한 스레드
}
is_valid, error = validate_config(cfg)
assert is_valid is True # 검증은 통과 (경고만)
# 경고 로그 확인
assert any("max_threads" in record.message and "과도" in record.message for record in caplog.records)
def test_min_order_value_too_low(self):
"""HIGH-002-4: 최소 주문 금액 < 5000원 시 실패"""
cfg = {
"buy_check_interval_minutes": 240,
"stop_loss_check_interval_minutes": 60,
"profit_taking_check_interval_minutes": 240,
"dry_run": True,
"auto_trade": {
"enabled": False,
"min_order_value_krw": 3000, # 너무 낮음
},
"confirm": {"confirm_stop_loss": False},
"max_threads": 3,
}
is_valid, error = validate_config(cfg)
assert is_valid is False
assert "min_order_value_krw" in error
assert "5000" in error
def test_buy_amount_less_than_min_order(self, caplog):
"""HIGH-002-5: buy_amount < min_order_value 시 경고"""
cfg = {
"buy_check_interval_minutes": 240,
"stop_loss_check_interval_minutes": 60,
"profit_taking_check_interval_minutes": 240,
"dry_run": True,
"auto_trade": {
"enabled": False,
"min_order_value_krw": 10000,
"buy_amount_krw": 5000, # min_order보다 작음
},
"confirm": {"confirm_stop_loss": False},
"max_threads": 3,
}
is_valid, error = validate_config(cfg)
assert is_valid is True # 검증은 통과 (경고만)
# 경고 로그 확인
assert any(
"buy_amount_krw" in record.message and "min_order_value_krw" in record.message for record in caplog.records
)
def test_buy_amount_too_low(self):
"""HIGH-002-5: buy_amount_krw < 5000원 시 실패"""
cfg = {
"buy_check_interval_minutes": 240,
"stop_loss_check_interval_minutes": 60,
"profit_taking_check_interval_minutes": 240,
"dry_run": True,
"auto_trade": {
"enabled": False,
"buy_amount_krw": 3000, # 너무 낮음
},
"confirm": {"confirm_stop_loss": False},
"max_threads": 3,
}
is_valid, error = validate_config(cfg)
assert is_valid is False
assert "buy_amount_krw" in error
assert "5000" in error
def test_confirm_invalid_type(self):
"""confirm 설정이 딕셔너리가 아니면 실패"""
cfg = {
"buy_check_interval_minutes": 240,
"stop_loss_check_interval_minutes": 60,
"profit_taking_check_interval_minutes": 240,
"dry_run": True,
"auto_trade": {"enabled": False},
"confirm": "invalid", # 잘못된 타입
"max_threads": 3,
}
is_valid, error = validate_config(cfg)
assert is_valid is False
assert "confirm" in error
def test_dry_run_invalid_type(self):
"""dry_run이 boolean이 아니면 실패"""
cfg = {
"buy_check_interval_minutes": 240,
"stop_loss_check_interval_minutes": 60,
"profit_taking_check_interval_minutes": 240,
"dry_run": "yes", # 잘못된 타입
"auto_trade": {"enabled": False},
"confirm": {"confirm_stop_loss": False},
}
is_valid, error = validate_config(cfg)
assert is_valid is False
assert "dry_run" in error
class TestEdgeCases:
"""경계값 및 엣지 케이스 테스트"""
def test_intervals_equal_one(self):
"""간격이 정확히 1일 때 (최소값)"""
cfg = {
"buy_check_interval_minutes": 1,
"stop_loss_check_interval_minutes": 1,
"profit_taking_check_interval_minutes": 1,
"dry_run": True,
"auto_trade": {"enabled": False},
"confirm": {"confirm_stop_loss": False},
"max_threads": 1,
}
is_valid, error = validate_config(cfg)
assert is_valid is True
def test_max_threads_equal_ten(self):
"""max_threads가 정확히 10일 때 (경계값)"""
cfg = {
"buy_check_interval_minutes": 240,
"stop_loss_check_interval_minutes": 60,
"profit_taking_check_interval_minutes": 240,
"dry_run": True,
"auto_trade": {"enabled": False},
"confirm": {"confirm_stop_loss": False},
"max_threads": 10, # 경계값
}
is_valid, error = validate_config(cfg)
assert is_valid is True # 10은 허용 (경고 없음)
def test_min_order_equal_5000(self):
"""최소 주문 금액이 정확히 5000원일 때"""
cfg = {
"buy_check_interval_minutes": 240,
"stop_loss_check_interval_minutes": 60,
"profit_taking_check_interval_minutes": 240,
"dry_run": True,
"auto_trade": {
"enabled": False,
"min_order_value_krw": 5000, # 최소값
},
"confirm": {"confirm_stop_loss": False},
"max_threads": 3,
}
is_valid, error = validate_config(cfg)
assert is_valid is True
def test_only_buy_enabled_without_enabled(self):
"""enabled=False, buy_enabled=True일 때도 API 키 체크"""
cfg = {
"buy_check_interval_minutes": 240,
"stop_loss_check_interval_minutes": 60,
"profit_taking_check_interval_minutes": 240,
"dry_run": False,
"auto_trade": {
"enabled": False,
"buy_enabled": True, # buy만 활성화
},
"confirm": {"confirm_stop_loss": False},
"max_threads": 3,
}
with patch.dict(os.environ, {}, clear=True):
is_valid, error = validate_config(cfg)
assert is_valid is False # API 키 필수
assert "UPBIT" in error