Files
awoooi/apps/api/tests/test_stats_api.py
OG T b7fb1d962f test(api): Stats API 單元測試 (12 cases)
測試項目:
- IncidentSummary: 空資料庫、解決率計算
- ResolutionStats: 無已解決事件
- IncidentTrends: 空資料、週期參數
- AIPerformance: 空 outcome、評分分佈初始化
- AffectedServices: 空結果、limit 參數
- FeedbackSummary: 空回饋、評分分類、主題萃取

首席架構師審查要求

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-24 10:14:40 +08:00

258 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# AWOOOI Statistics API 單元測試
# =============================================================================
# Phase 6.5: 測試統計分析 API 端點
#
# 測試項目:
# - /stats/incidents/summary
# - /stats/incidents/resolution
# - /stats/incidents/trends
# - /stats/ai-performance
# - /stats/services/affected
# - /stats/feedback/summary
# =============================================================================
import pytest
from datetime import datetime, timedelta
from unittest.mock import AsyncMock, MagicMock, patch
from src.api.v1.stats import (
get_incident_summary,
get_resolution_stats,
get_incident_trends,
get_ai_performance,
get_affected_services,
get_feedback_summary,
IncidentSummary,
ResolutionStats,
IncidentTrends,
AIPerformance,
ServiceImpact,
FeedbackSummary,
)
class TestIncidentSummary:
"""事件總覽統計測試"""
@pytest.mark.asyncio
async def test_empty_database_returns_zero_counts(self):
"""空資料庫應返回零計數"""
mock_db = AsyncMock()
mock_db.execute = AsyncMock(return_value=MagicMock(
scalar=MagicMock(return_value=0),
all=MagicMock(return_value=[])
))
result = await get_incident_summary(days=30, db=mock_db)
assert result.total_incidents == 0
assert result.resolved_rate == 0.0
assert len(result.status_distribution) == 0
assert len(result.severity_distribution) == 0
@pytest.mark.asyncio
async def test_resolved_rate_calculation(self):
"""解決率計算正確"""
# 10 總數3 已解決 = 30%
mock_db = AsyncMock()
call_count = 0
def mock_execute(*args, **kwargs):
nonlocal call_count
call_count += 1
result = MagicMock()
if call_count == 1: # 總數
result.scalar.return_value = 10
elif call_count == 2: # 狀態分佈
result.all.return_value = []
elif call_count == 3: # 嚴重度分佈
result.all.return_value = []
elif call_count == 4: # 已解決數
result.scalar.return_value = 3
else: # 平均 signals
result.scalar.return_value = 2.5
return result
mock_db.execute = AsyncMock(side_effect=mock_execute)
result = await get_incident_summary(days=30, db=mock_db)
assert result.total_incidents == 10
assert result.resolved_rate == 30.0
class TestResolutionStats:
"""解決時間統計測試"""
@pytest.mark.asyncio
async def test_empty_results_return_none(self):
"""無已解決事件應返回 None"""
mock_db = AsyncMock()
mock_db.execute = AsyncMock(return_value=MagicMock(
all=MagicMock(return_value=[])
))
result = await get_resolution_stats(days=30, db=mock_db)
assert result.avg_minutes is None
assert result.p50_minutes is None
assert result.p95_minutes is None
assert result.sample_size == 0
class TestIncidentTrends:
"""事件趨勢測試"""
@pytest.mark.asyncio
async def test_empty_data_returns_empty_list(self):
"""無資料應返回空列表"""
mock_db = AsyncMock()
mock_db.execute = AsyncMock(return_value=MagicMock(
all=MagicMock(return_value=[])
))
result = await get_incident_trends(days=30, period="daily", db=mock_db)
assert result.period == "daily"
assert len(result.data) == 0
@pytest.mark.asyncio
async def test_period_validation(self):
"""週期參數驗證"""
mock_db = AsyncMock()
mock_db.execute = AsyncMock(return_value=MagicMock(
all=MagicMock(return_value=[])
))
for period in ["daily", "weekly", "monthly"]:
result = await get_incident_trends(days=30, period=period, db=mock_db)
assert result.period == period
class TestAIPerformance:
"""AI 效能統計測試"""
@pytest.mark.asyncio
async def test_empty_outcomes(self):
"""無 outcome 資料應返回零"""
mock_db = AsyncMock()
mock_db.execute = AsyncMock(return_value=MagicMock(
all=MagicMock(return_value=[])
))
result = await get_ai_performance(days=30, db=mock_db)
assert result.total_proposals == 0
assert result.executed_count == 0
assert result.execution_rate == 0.0
assert result.success_rate == 0.0
assert result.avg_effectiveness is None
@pytest.mark.asyncio
async def test_effectiveness_distribution_initialized(self):
"""有效性評分分佈初始化正確"""
mock_db = AsyncMock()
mock_db.execute = AsyncMock(return_value=MagicMock(
all=MagicMock(return_value=[])
))
result = await get_ai_performance(days=30, db=mock_db)
# 應該有 1-5 的所有評分
assert set(result.effectiveness_distribution.keys()) == {1, 2, 3, 4, 5}
class TestAffectedServices:
"""受影響服務統計測試"""
@pytest.mark.asyncio
async def test_empty_results(self):
"""無資料應返回空列表"""
mock_db = AsyncMock()
mock_db.execute = AsyncMock(return_value=MagicMock(
all=MagicMock(return_value=[])
))
result = await get_affected_services(days=30, limit=10, db=mock_db)
assert len(result) == 0
@pytest.mark.asyncio
async def test_limit_parameter(self):
"""limit 參數應限制結果數量"""
mock_db = AsyncMock()
# 模擬 5 個服務的資料
mock_db.execute = AsyncMock(return_value=MagicMock(
all=MagicMock(return_value=[
(["svc1"], "P0"),
(["svc2"], "P1"),
(["svc3"], "P2"),
(["svc1"], "P0"), # svc1 重複
(["svc2"], "P1"), # svc2 重複
])
))
result = await get_affected_services(days=30, limit=2, db=mock_db)
# 應該只返回前 2 個 (按計數排序)
assert len(result) <= 2
class TestFeedbackSummary:
"""人類回饋摘要測試"""
@pytest.mark.asyncio
async def test_empty_feedback(self):
"""無回饋應返回零計數"""
mock_db = AsyncMock()
mock_db.execute = AsyncMock(return_value=MagicMock(
all=MagicMock(return_value=[])
))
result = await get_feedback_summary(days=30, db=mock_db)
assert result.total_feedback == 0
assert result.positive_count == 0
assert result.neutral_count == 0
assert result.negative_count == 0
assert len(result.common_themes) == 0
@pytest.mark.asyncio
async def test_score_categorization(self):
"""評分分類正確 (>=4 正面, =3 中性, <=2 負面)"""
mock_db = AsyncMock()
mock_db.execute = AsyncMock(return_value=MagicMock(
all=MagicMock(return_value=[
({"effectiveness_score": 5},), # 正面
({"effectiveness_score": 4},), # 正面
({"effectiveness_score": 3},), # 中性
({"effectiveness_score": 2},), # 負面
({"effectiveness_score": 1},), # 負面
])
))
result = await get_feedback_summary(days=30, db=mock_db)
assert result.positive_count == 2
assert result.neutral_count == 1
assert result.negative_count == 2
assert result.total_feedback == 5
@pytest.mark.asyncio
async def test_theme_extraction(self):
"""主題萃取包含關鍵字"""
mock_db = AsyncMock()
mock_db.execute = AsyncMock(return_value=MagicMock(
all=MagicMock(return_value=[
({"learning_notes": "Connection timeout issue"},),
({"learning_notes": "Memory leak detected"},),
({"learning_notes": "timeout again"},),
])
))
result = await get_feedback_summary(days=30, db=mock_db)
# timeout 出現 2 次,應該在常見主題中
assert "timeout" in result.common_themes or "connection" in result.common_themes