From cdc364cb2a98a1297b91c1c8a0455eaed90ac344 Mon Sep 17 00:00:00 2001 From: zfc Date: Wed, 28 Jan 2026 14:44:54 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E6=8F=90=E5=8D=87=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E7=8E=87=E8=87=B3=2099%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 brand_api 测试: 覆盖所有异常处理分支 - 新增 main.py 测试: root 和 health 端点 - 新增 logging 测试: setup_logging 和 get_logger - 新增 KolVideo __repr__ 测试 - 总计 67 个测试全部通过 Co-Authored-By: Claude Opus 4.5 --- backend/tests/test_brand_api.py | 124 +++++++++++++++++++++++++++++++- backend/tests/test_database.py | 27 +++++++ backend/tests/test_main.py | 30 ++++++++ 3 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 backend/tests/test_main.py diff --git a/backend/tests/test_brand_api.py b/backend/tests/test_brand_api.py index 20c0cb1..8d02276 100644 --- a/backend/tests/test_brand_api.py +++ b/backend/tests/test_brand_api.py @@ -1,6 +1,6 @@ import pytest import asyncio -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, patch, MagicMock import httpx from app.services.brand_api import get_brand_names, fetch_brand_name @@ -115,3 +115,125 @@ class TestBrandAPI: assert len(result) == 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 diff --git a/backend/tests/test_database.py b/backend/tests/test_database.py index 877f12a..b8084fb 100644 --- a/backend/tests/test_database.py +++ b/backend/tests/test_database.py @@ -2,6 +2,22 @@ import pytest from sqlalchemy import select 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: @@ -163,3 +179,14 @@ class TestKolVideoModel: select(KolVideo).where(KolVideo.item_id == sample_video_data["item_id"]) ) 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 diff --git a/backend/tests/test_main.py b/backend/tests/test_main.py new file mode 100644 index 0000000..6b2bc3f --- /dev/null +++ b/backend/tests/test_main.py @@ -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"