테스트 강화 및 코드 품질 개선
This commit is contained in:
328
src/tests/test_config_validation.py
Normal file
328
src/tests/test_config_validation.py
Normal 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
|
||||
Reference in New Issue
Block a user