test: 提升测试覆盖率至 99%
- 新增 brand_api 测试: 覆盖所有异常处理分支 - 新增 main.py 测试: root 和 health 端点 - 新增 logging 测试: setup_logging 和 get_logger - 新增 KolVideo __repr__ 测试 - 总计 67 个测试全部通过 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d838a9bea2
commit
cdc364cb2a
@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import asyncio
|
import asyncio
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch, MagicMock
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from app.services.brand_api import get_brand_names, fetch_brand_name
|
from app.services.brand_api import get_brand_names, fetch_brand_name
|
||||||
@ -115,3 +115,125 @@ class TestBrandAPI:
|
|||||||
assert len(result) == 15
|
assert len(result) == 15
|
||||||
# 验证所有调用都完成了
|
# 验证所有调用都完成了
|
||||||
assert mock_fetch.call_count == 15
|
assert mock_fetch.call_count == 15
|
||||||
|
|
||||||
|
async def test_fetch_brand_name_200_with_nested_data(self):
|
||||||
|
"""Test successful brand fetch with nested data structure."""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {"data": {"name": "嵌套品牌名"}}
|
||||||
|
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.get.return_value = mock_response
|
||||||
|
mock_client.__aenter__.return_value = mock_client
|
||||||
|
mock_client.__aexit__.return_value = None
|
||||||
|
|
||||||
|
with patch("httpx.AsyncClient", return_value=mock_client):
|
||||||
|
semaphore = asyncio.Semaphore(10)
|
||||||
|
brand_id, brand_name = await fetch_brand_name("brand_nested", semaphore)
|
||||||
|
|
||||||
|
assert brand_id == "brand_nested"
|
||||||
|
assert brand_name == "嵌套品牌名"
|
||||||
|
|
||||||
|
async def test_fetch_brand_name_200_with_flat_data(self):
|
||||||
|
"""Test successful brand fetch with flat data structure."""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {"name": "扁平品牌名"}
|
||||||
|
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.get.return_value = mock_response
|
||||||
|
mock_client.__aenter__.return_value = mock_client
|
||||||
|
mock_client.__aexit__.return_value = None
|
||||||
|
|
||||||
|
with patch("httpx.AsyncClient", return_value=mock_client):
|
||||||
|
semaphore = asyncio.Semaphore(10)
|
||||||
|
brand_id, brand_name = await fetch_brand_name("brand_flat", semaphore)
|
||||||
|
|
||||||
|
assert brand_id == "brand_flat"
|
||||||
|
assert brand_name == "扁平品牌名"
|
||||||
|
|
||||||
|
async def test_fetch_brand_name_200_no_name(self):
|
||||||
|
"""Test brand fetch with 200 but no name in response."""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {"data": {"id": "123"}} # No name field
|
||||||
|
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.get.return_value = mock_response
|
||||||
|
mock_client.__aenter__.return_value = mock_client
|
||||||
|
mock_client.__aexit__.return_value = None
|
||||||
|
|
||||||
|
with patch("httpx.AsyncClient", return_value=mock_client):
|
||||||
|
semaphore = asyncio.Semaphore(10)
|
||||||
|
brand_id, brand_name = await fetch_brand_name("brand_no_name", semaphore)
|
||||||
|
|
||||||
|
assert brand_id == "brand_no_name"
|
||||||
|
assert brand_name == "brand_no_name" # Fallback
|
||||||
|
|
||||||
|
async def test_fetch_brand_name_request_error(self):
|
||||||
|
"""Test brand fetch with request error."""
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.get.side_effect = httpx.RequestError("Connection failed")
|
||||||
|
mock_client.__aenter__.return_value = mock_client
|
||||||
|
mock_client.__aexit__.return_value = None
|
||||||
|
|
||||||
|
with patch("httpx.AsyncClient", return_value=mock_client):
|
||||||
|
semaphore = asyncio.Semaphore(10)
|
||||||
|
brand_id, brand_name = await fetch_brand_name("brand_error", semaphore)
|
||||||
|
|
||||||
|
assert brand_id == "brand_error"
|
||||||
|
assert brand_name == "brand_error" # Fallback
|
||||||
|
|
||||||
|
async def test_fetch_brand_name_unexpected_error(self):
|
||||||
|
"""Test brand fetch with unexpected error."""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.side_effect = ValueError("Invalid JSON")
|
||||||
|
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.get.return_value = mock_response
|
||||||
|
mock_client.__aenter__.return_value = mock_client
|
||||||
|
mock_client.__aexit__.return_value = None
|
||||||
|
|
||||||
|
with patch("httpx.AsyncClient", return_value=mock_client):
|
||||||
|
semaphore = asyncio.Semaphore(10)
|
||||||
|
brand_id, brand_name = await fetch_brand_name("brand_json_error", semaphore)
|
||||||
|
|
||||||
|
assert brand_id == "brand_json_error"
|
||||||
|
assert brand_name == "brand_json_error" # Fallback
|
||||||
|
|
||||||
|
async def test_get_brand_names_with_exception_in_gather(self):
|
||||||
|
"""Test that exceptions in asyncio.gather are handled."""
|
||||||
|
with patch("app.services.brand_api.fetch_brand_name") as mock_fetch:
|
||||||
|
# 第二个调用抛出异常
|
||||||
|
mock_fetch.side_effect = [
|
||||||
|
("brand_001", "品牌A"),
|
||||||
|
Exception("Unexpected error"),
|
||||||
|
("brand_003", "品牌C"),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = await get_brand_names(["brand_001", "brand_002", "brand_003"])
|
||||||
|
|
||||||
|
# 成功的应该在结果中
|
||||||
|
assert result["brand_001"] == "品牌A"
|
||||||
|
assert result["brand_003"] == "品牌C"
|
||||||
|
# 失败的不应该在结果中(因为是 Exception,不是 tuple)
|
||||||
|
assert "brand_002" not in result
|
||||||
|
|
||||||
|
async def test_fetch_brand_name_non_dict_response(self):
|
||||||
|
"""Test brand fetch with non-dict response."""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = ["not", "a", "dict"] # Array instead of dict
|
||||||
|
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.get.return_value = mock_response
|
||||||
|
mock_client.__aenter__.return_value = mock_client
|
||||||
|
mock_client.__aexit__.return_value = None
|
||||||
|
|
||||||
|
with patch("httpx.AsyncClient", return_value=mock_client):
|
||||||
|
semaphore = asyncio.Semaphore(10)
|
||||||
|
brand_id, brand_name = await fetch_brand_name("brand_array", semaphore)
|
||||||
|
|
||||||
|
assert brand_id == "brand_array"
|
||||||
|
assert brand_name == "brand_array" # Fallback because response is not dict
|
||||||
|
|||||||
@ -2,6 +2,22 @@ import pytest
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
from app.models import KolVideo
|
from app.models import KolVideo
|
||||||
|
from app.core.logging import setup_logging, get_logger
|
||||||
|
|
||||||
|
|
||||||
|
class TestLogging:
|
||||||
|
"""Tests for logging module."""
|
||||||
|
|
||||||
|
def test_setup_logging(self):
|
||||||
|
"""Test logging setup."""
|
||||||
|
# Should not raise
|
||||||
|
setup_logging()
|
||||||
|
|
||||||
|
def test_get_logger(self):
|
||||||
|
"""Test getting a logger."""
|
||||||
|
logger = get_logger("test")
|
||||||
|
assert logger is not None
|
||||||
|
assert logger.name == "test"
|
||||||
|
|
||||||
|
|
||||||
class TestKolVideoModel:
|
class TestKolVideoModel:
|
||||||
@ -163,3 +179,14 @@ class TestKolVideoModel:
|
|||||||
select(KolVideo).where(KolVideo.item_id == sample_video_data["item_id"])
|
select(KolVideo).where(KolVideo.item_id == sample_video_data["item_id"])
|
||||||
)
|
)
|
||||||
assert result.scalar_one_or_none() is None
|
assert result.scalar_one_or_none() is None
|
||||||
|
|
||||||
|
async def test_video_repr(self, test_session, sample_video_data):
|
||||||
|
"""Test KolVideo __repr__ method."""
|
||||||
|
video = KolVideo(**sample_video_data)
|
||||||
|
test_session.add(video)
|
||||||
|
await test_session.commit()
|
||||||
|
|
||||||
|
repr_str = repr(video)
|
||||||
|
assert "KolVideo" in repr_str
|
||||||
|
assert sample_video_data["item_id"] in repr_str
|
||||||
|
assert sample_video_data["title"] in repr_str
|
||||||
|
|||||||
30
backend/tests/test_main.py
Normal file
30
backend/tests/test_main.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import pytest
|
||||||
|
from httpx import AsyncClient, ASGITransport
|
||||||
|
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
|
||||||
|
class TestMainApp:
|
||||||
|
"""Tests for main app endpoints."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def client(self):
|
||||||
|
"""Create test client."""
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||||
|
yield ac
|
||||||
|
|
||||||
|
async def test_root_endpoint(self, client):
|
||||||
|
"""Test root endpoint returns app info."""
|
||||||
|
response = await client.get("/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["message"] == "KOL Insight API"
|
||||||
|
assert data["version"] == "1.0.0"
|
||||||
|
|
||||||
|
async def test_health_endpoint(self, client):
|
||||||
|
"""Test health check endpoint."""
|
||||||
|
response = await client.get("/health")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["status"] == "healthy"
|
||||||
Loading…
x
Reference in New Issue
Block a user