test_market_tools.pyβ’11.9 kB
"""μμ₯ κ°μ λꡬ ν
μ€νΈ"""
import pytest
from datetime import datetime, timedelta
from unittest.mock import Mock, AsyncMock, patch
from src.tools.market_tools import MarketOverviewTool
from src.exceptions import DataValidationError, DatabaseConnectionError
class TestMarketOverviewTool:
"""μμ₯ κ°μ λꡬ ν
μ€νΈ"""
@pytest.fixture
def mock_db_manager(self):
"""Mock λ°μ΄ν°λ² μ΄μ€ λ§€λμ """
db_manager = AsyncMock()
return db_manager
@pytest.fixture
def mock_cache_manager(self):
"""Mock μΊμ λ§€λμ """
cache_manager = AsyncMock()
return cache_manager
@pytest.fixture
def market_tool(self, mock_db_manager, mock_cache_manager):
"""μμ₯ κ°μ λꡬ μΈμ€ν΄μ€"""
return MarketOverviewTool(mock_db_manager, mock_cache_manager)
@pytest.fixture
def sample_market_data(self):
"""μν μμ₯ λ°μ΄ν°"""
return [
{
"index_code": "KOSPI",
"current_value": 2650.50,
"change": 15.30,
"change_rate": 0.58,
"volume": 450000000,
"market_cap": 1850000000000000,
"timestamp": datetime.now()
},
{
"index_code": "KOSDAQ",
"current_value": 850.25,
"change": -5.75,
"change_rate": -0.67,
"volume": 180000000,
"market_cap": 285000000000000,
"timestamp": datetime.now()
}
]
def test_tool_initialization(self, market_tool, mock_db_manager, mock_cache_manager):
"""λꡬ μ΄κΈ°ν ν
μ€νΈ"""
assert market_tool.name == "get_market_overview"
assert market_tool.description is not None
assert "μμ₯" in market_tool.description
assert market_tool.db_manager == mock_db_manager
assert market_tool.cache_manager == mock_cache_manager
def test_tool_definition(self, market_tool):
"""λꡬ μ μ ν
μ€νΈ"""
definition = market_tool.get_tool_definition()
assert definition.name == "get_market_overview"
assert definition.description is not None
assert definition.inputSchema is not None
# μ
λ ₯ μ€ν€λ§ κ²μ¦
schema = definition.inputSchema
assert schema["type"] == "object"
assert "properties" in schema
properties = schema["properties"]
assert "market" in properties
assert "include_details" in properties
# market νλΌλ―Έν° κ²μ¦
market_prop = properties["market"]
assert market_prop["type"] == "string"
assert "enum" in market_prop
assert "KOSPI" in market_prop["enum"]
assert "KOSDAQ" in market_prop["enum"]
assert "ALL" in market_prop["enum"]
@pytest.mark.asyncio
async def test_execute_basic_overview_with_cache_miss(self, market_tool, sample_market_data):
"""μΊμ λ―Έμ€ μ κΈ°λ³Έ μμ₯ κ°μ μ‘°ν ν
μ€νΈ"""
# μΊμ λ―Έμ€ μ€μ
market_tool.cache_manager.get.return_value = None
# λ°μ΄ν°λ² μ΄μ€ μλ΅ μ€μ
market_tool.db_manager.fetch_all.return_value = sample_market_data
# μ€ν
result = await market_tool.execute({"market": "ALL", "include_details": False})
# κ²°κ³Ό κ²μ¦
assert len(result) == 1
content = result[0]
assert content.type == "text"
# JSON νμ±νμ¬ λ΄μ© νμΈ
import json
data = json.loads(content.text)
assert "timestamp" in data
assert "market_summary" in data
assert "indices" in data
# μ§μ λ°μ΄ν° κ²μ¦
indices = data["indices"]
assert len(indices) == 2
kospi_data = next((idx for idx in indices if idx["index_code"] == "KOSPI"), None)
assert kospi_data is not None
assert kospi_data["current_value"] == 2650.50
assert kospi_data["change"] == 15.30
assert kospi_data["change_rate"] == 0.58
# μΊμ μ μ₯ νμΈ
market_tool.cache_manager.set.assert_called_once()
# λ°μ΄ν°λ² μ΄μ€ 쿼리 νμΈ
market_tool.db_manager.fetch_all.assert_called_once()
@pytest.mark.asyncio
async def test_execute_with_cache_hit(self, market_tool):
"""μΊμ ννΈ μ ν
μ€νΈ"""
# μΊμ λ°μ΄ν° μ€μ
cached_data = {
"timestamp": datetime.now().isoformat(),
"market_summary": {"total_market_cap": 2000000000000000},
"indices": [{"index_code": "KOSPI", "current_value": 2650.50}]
}
market_tool.cache_manager.get.return_value = cached_data
# μ€ν
result = await market_tool.execute({"market": "KOSPI"})
# κ²°κ³Ό κ²μ¦
assert len(result) == 1
content = result[0]
import json
data = json.loads(content.text)
assert data == cached_data
# λ°μ΄ν°λ² μ΄μ€ νΈμΆμ΄ μμ΄μΌ ν¨
market_tool.db_manager.fetch_all.assert_not_called()
# μΊμ μ μ₯ νΈμΆμ΄ μμ΄μΌ ν¨
market_tool.cache_manager.set.assert_not_called()
@pytest.mark.asyncio
async def test_execute_with_details(self, market_tool, sample_market_data):
"""μμΈ μ 보 ν¬ν¨ μ‘°ν ν
μ€νΈ"""
# μΊμ λ―Έμ€
market_tool.cache_manager.get.return_value = None
# λ©μΈ λ°μ΄ν°
market_tool.db_manager.fetch_all.return_value = sample_market_data
# μΆκ° μμΈ λ°μ΄ν° Mock
market_tool.db_manager.fetch_one.side_effect = [
{"advancing": 850, "declining": 680, "unchanged": 120}, # μμ₯ ν λ°μ΄ν°
{"new_highs": 45, "new_lows": 23}, # μ κ³ κ°/μ μ κ° λ°μ΄ν°
]
# μ€ν
result = await market_tool.execute({
"market": "ALL",
"include_details": True
})
# κ²°κ³Ό κ²μ¦
content = result[0]
import json
data = json.loads(content.text)
assert "market_breadth" in data
assert "new_highs_lows" in data
# μμΈ λ°μ΄ν° κ²μ¦
breadth = data["market_breadth"]
assert breadth["advancing"] == 850
assert breadth["declining"] == 680
highs_lows = data["new_highs_lows"]
assert highs_lows["new_highs"] == 45
assert highs_lows["new_lows"] == 23
# λ€μ€ λ°μ΄ν°λ² μ΄μ€ 쿼리 νμΈ
assert market_tool.db_manager.fetch_all.call_count == 1
assert market_tool.db_manager.fetch_one.call_count == 2
@pytest.mark.asyncio
async def test_execute_specific_market(self, market_tool, sample_market_data):
"""νΉμ μμ₯ μ‘°ν ν
μ€νΈ"""
# KOSPIλ§ νν°λ§λ λ°μ΄ν°
kospi_data = [data for data in sample_market_data if data["index_code"] == "KOSPI"]
market_tool.cache_manager.get.return_value = None
market_tool.db_manager.fetch_all.return_value = kospi_data
# μ€ν
result = await market_tool.execute({"market": "KOSPI"})
# κ²°κ³Ό κ²μ¦
content = result[0]
import json
data = json.loads(content.text)
indices = data["indices"]
assert len(indices) == 1
assert indices[0]["index_code"] == "KOSPI"
# μ¬λ°λ₯Έ WHERE μ λ‘ μΏΌλ¦¬λμλμ§ νμΈ
call_args = market_tool.db_manager.fetch_all.call_args[0]
query = call_args[0]
assert "WHERE" in query.upper()
assert "KOSPI" in call_args[1] or "kospi" in str(call_args).lower()
@pytest.mark.asyncio
async def test_database_error_handling(self, market_tool):
"""λ°μ΄ν°λ² μ΄μ€ μ€λ₯ μ²λ¦¬ ν
μ€νΈ"""
market_tool.cache_manager.get.return_value = None
market_tool.db_manager.fetch_all.side_effect = DatabaseConnectionError("DB μ°κ²° μ€ν¨")
with pytest.raises(DatabaseConnectionError):
await market_tool.execute({"market": "ALL"})
@pytest.mark.asyncio
async def test_empty_data_handling(self, market_tool):
"""λΉ λ°μ΄ν° μ²λ¦¬ ν
μ€νΈ"""
market_tool.cache_manager.get.return_value = None
market_tool.db_manager.fetch_all.return_value = []
result = await market_tool.execute({"market": "ALL"})
content = result[0]
import json
data = json.loads(content.text)
assert data["indices"] == []
assert "message" in data
assert "λ°μ΄ν°κ° μμ΅λλ€" in data["message"] or "no data" in data["message"].lower()
@pytest.mark.asyncio
async def test_invalid_market_parameter(self, market_tool):
"""μλͺ»λ μμ₯ νλΌλ―Έν° ν
μ€νΈ"""
with pytest.raises(ValueError, match="Invalid market"):
await market_tool.execute({"market": "INVALID_MARKET"})
@pytest.mark.asyncio
async def test_cache_key_generation(self, market_tool):
"""μΊμ ν€ μμ± ν
μ€νΈ"""
market_tool.cache_manager.get.return_value = None
market_tool.db_manager.fetch_all.return_value = []
await market_tool.execute({"market": "KOSPI", "include_details": True})
# μΊμ μ‘°ν μ μ¬λ°λ₯Έ ν€ μ¬μ© νμΈ
cache_key = market_tool.cache_manager.get.call_args[0][0]
assert "market_overview" in cache_key
assert "KOSPI" in cache_key
assert "True" in cache_key # include_details
@pytest.mark.asyncio
async def test_data_freshness_validation(self, market_tool):
"""λ°μ΄ν° μ μ λ κ²μ¦ ν
μ€νΈ"""
# 5λΆ μ λ°μ΄ν° (λ§λ£λμ§ μμ)
fresh_time = datetime.now() - timedelta(minutes=3)
market_tool.cache_manager.get.return_value = {
"timestamp": fresh_time.isoformat(),
"indices": []
}
result = await market_tool.execute({"market": "ALL"})
# μΊμ λ°μ΄ν° μ¬μ©λ¨
market_tool.db_manager.fetch_all.assert_not_called()
# 10λΆ μ λ°μ΄ν° (λ§λ£λ¨) - μλ‘μ΄ λꡬ μΈμ€ν΄μ€λ‘ ν
μ€νΈ
market_tool.cache_manager.reset_mock()
market_tool.db_manager.reset_mock()
stale_time = datetime.now() - timedelta(minutes=10)
market_tool.cache_manager.get.return_value = {
"timestamp": stale_time.isoformat(),
"indices": []
}
market_tool.db_manager.fetch_all.return_value = []
await market_tool.execute({"market": "ALL"})
# λ°μ΄ν°λ² μ΄μ€μμ μλ‘ μ‘°νλ¨
market_tool.db_manager.fetch_all.assert_called_once()
@pytest.mark.asyncio
async def test_concurrent_execution_safety(self, market_tool, sample_market_data):
"""λμ μ€ν μμ μ± ν
μ€νΈ"""
import asyncio
market_tool.cache_manager.get.return_value = None
market_tool.db_manager.fetch_all.return_value = sample_market_data
# λμμ μ¬λ¬ μμ² μ€ν
tasks = [
market_tool.execute({"market": "ALL"})
for _ in range(5)
]
results = await asyncio.gather(*tasks)
# λͺ¨λ κ²°κ³Όκ° λμΌν΄μΌ ν¨
assert len(results) == 5
for result in results:
assert len(result) == 1
content = result[0]
import json
data = json.loads(content.text)
assert len(data["indices"]) == 2