-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_sentiment.py
More file actions
161 lines (137 loc) · 5.34 KB
/
test_sentiment.py
File metadata and controls
161 lines (137 loc) · 5.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
"""
tests/test_sentiment.py — 情绪分析引擎单元测试
覆盖:
- SentimentAnalyzer 缓存逻辑
- Claude API Mock 调用
- 批量分析输入/输出格式验证
- 情绪标签与分值一致性检查
"""
from __future__ import annotations
import json
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from src.analysis.sentiment import SentimentAnalyzer
class TestSentimentAnalyzer:
"""SentimentAnalyzer 单元测试。"""
@pytest.fixture
def analyzer(self) -> SentimentAnalyzer:
"""创建 SentimentAnalyzer 实例。"""
return SentimentAnalyzer()
@pytest.fixture
def mock_claude_response(self) -> dict[str, Any]:
"""Mock Claude API 返回的情绪分析结果。"""
return {
"results": [
{
"index": 0,
"score": 65,
"sentiment": "bullish",
"topics": ["Mintegral增速", "东南亚扩张"],
"confidence": 0.88,
"reasoning": "内容明确表达看好情绪",
},
{
"index": 1,
"score": 50,
"sentiment": "bullish",
"topics": ["估值洼地", "竞对对比"],
"confidence": 0.75,
"reasoning": "与竞对相比估值偏低",
},
{
"index": 2,
"score": -30,
"sentiment": "bearish",
"topics": ["广告行业", "宏观压力"],
"confidence": 0.70,
"reasoning": "提及行业整体低迷",
},
]
}
@pytest.mark.asyncio
async def test_analyze_batch_returns_enriched_records(
self,
analyzer: SentimentAnalyzer,
sample_sentiment_records: list[dict[str, Any]],
mock_claude_response: dict[str, Any],
mocker: Any,
) -> None:
"""测试批量分析返回包含情绪字段的增强记录。"""
# 内存缓存为空(全部未命中)
analyzer._cache.clear()
# Mock Claude API 调用
mock_response = MagicMock()
mock_response.content = [MagicMock(text=json.dumps(mock_claude_response))]
mock_client = mocker.AsyncMock()
mock_client.messages.create.return_value = mock_response
mocker.patch.object(analyzer, "_get_client", return_value=mock_client)
results = await analyzer.analyze_batch(sample_sentiment_records)
assert len(results) == len(sample_sentiment_records)
for result in results:
assert "score" in result
assert "sentiment" in result
assert "topics" in result
assert "confidence" in result
assert "analyzed_at" in result
@pytest.mark.asyncio
async def test_analyze_empty_returns_empty(self, analyzer: SentimentAnalyzer) -> None:
"""测试空输入返回空列表。"""
results = await analyzer.analyze_batch([])
assert results == []
@pytest.mark.asyncio
async def test_cache_hit_skips_api_call(
self,
analyzer: SentimentAnalyzer,
mocker: Any,
) -> None:
"""测试缓存命中时不调用 Claude API。"""
cached_result = {
"score": 70,
"sentiment": "bullish",
"topics": ["cached_topic"],
"confidence": 0.9,
}
content = "测试内容"
# 直接写入内存缓存
analyzer._cache[analyzer._cache_key(content)] = cached_result
# Mock Claude(不应被调用)
mock_client = mocker.AsyncMock()
mocker.patch.object(analyzer, "_get_client", return_value=mock_client)
records = [{"content": content, "platform": "xueqiu"}]
results = await analyzer.analyze_batch(records)
assert len(results) == 1
assert results[0]["score"] == 70
# 验证 Claude API 未被调用
mock_client.messages.create.assert_not_called()
def test_cache_key_is_deterministic(self, analyzer: SentimentAnalyzer) -> None:
"""测试相同内容始终生成相同缓存键。"""
content = "测试内容:汇量科技看涨"
key1 = analyzer._cache_key(content)
key2 = analyzer._cache_key(content)
assert key1 == key2
assert key1.startswith("sentiment:")
def test_cache_key_differs_for_different_content(self, analyzer: SentimentAnalyzer) -> None:
"""测试不同内容生成不同缓存键。"""
key1 = analyzer._cache_key("内容A")
key2 = analyzer._cache_key("内容B")
assert key1 != key2
@pytest.mark.asyncio
async def test_analyze_single(
self,
analyzer: SentimentAnalyzer,
mocker: Any,
) -> None:
"""测试单条分析接口。"""
mock_result = {"score": 45, "sentiment": "bullish", "topics": [], "confidence": 0.8}
# Mock analyze_batch
mocker.patch.object(
analyzer,
"analyze_batch",
return_value=[
{"content": "test", **mock_result, "analyzed_at": "2025-03-15T10:00:00Z"}
],
)
result = await analyzer.analyze_single("test")
assert result["score"] == 45
assert result["sentiment"] == "bullish"