Files
smart-project-pricing/后端服务/tests/test_services/test_pricing_service.py
2026-01-31 21:33:06 +08:00

370 lines
12 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.
"""智能定价服务单元测试
测试 PricingService 的核心业务逻辑
"""
import pytest
from decimal import Decimal
from datetime import datetime
from unittest.mock import AsyncMock, patch, MagicMock
from sqlalchemy.ext.asyncio import AsyncSession
from app.services.pricing_service import PricingService
from app.schemas.pricing import (
StrategyType, MarketReference, StrategySuggestion, PricingSuggestions
)
from app.models import Project, ProjectCostSummary, PricingPlan
class TestPricingService:
"""智能定价服务测试类"""
@pytest.mark.unit
def test_calculate_strategy_price_traffic(self):
"""测试引流款定价策略"""
service = PricingService(None)
suggestion = service.calculate_strategy_price(
base_cost=100.0,
strategy=StrategyType.TRAFFIC,
)
# 引流款利润率 10%-20%,使用中间值 15%
# 价格 = 100 / (1 - 0.15) ≈ 117.65
assert suggestion.strategy == "引流款"
assert suggestion.suggested_price > 100 # 大于成本
assert suggestion.suggested_price < 130 # 利润率适中
assert suggestion.margin > 0
assert "引流" in suggestion.description
@pytest.mark.unit
def test_calculate_strategy_price_profit(self):
"""测试利润款定价策略"""
service = PricingService(None)
suggestion = service.calculate_strategy_price(
base_cost=100.0,
strategy=StrategyType.PROFIT,
target_margin=50, # 50% 目标毛利率
)
# 价格 = 100 / (1 - 0.5) = 200
assert suggestion.strategy == "利润款"
assert suggestion.suggested_price >= 200
assert suggestion.margin >= 45 # 接近目标
assert "日常" in suggestion.description
@pytest.mark.unit
def test_calculate_strategy_price_premium(self):
"""测试高端款定价策略"""
service = PricingService(None)
suggestion = service.calculate_strategy_price(
base_cost=100.0,
strategy=StrategyType.PREMIUM,
)
# 高端款利润率 60%-80%,使用中间值 70%
# 价格 = 100 / (1 - 0.7) ≈ 333
assert suggestion.strategy == "高端款"
assert suggestion.suggested_price > 300
assert suggestion.margin > 60
assert "高端" in suggestion.description
@pytest.mark.unit
def test_calculate_strategy_price_with_market_reference(self):
"""测试带市场参考的定价"""
service = PricingService(None)
market_ref = MarketReference(min=80.0, max=150.0, avg=100.0)
# 引流款应该参考市场最低价
suggestion = service.calculate_strategy_price(
base_cost=50.0,
strategy=StrategyType.TRAFFIC,
market_ref=market_ref,
)
# 应该取市场最低价的 90% 和成本定价的较低者
assert suggestion.suggested_price <= 100 # 不会太高
assert suggestion.suggested_price >= 50 * 1.05 # 不低于成本
@pytest.mark.unit
def test_calculate_strategy_price_ensures_profit(self):
"""测试确保价格不低于成本"""
service = PricingService(None)
market_ref = MarketReference(min=30.0, max=50.0, avg=40.0)
# 即使市场价很低,也不能低于成本
suggestion = service.calculate_strategy_price(
base_cost=100.0, # 成本高于市场价
strategy=StrategyType.TRAFFIC,
market_ref=market_ref,
)
# 价格至少是成本的 1.05 倍
assert suggestion.suggested_price >= 100 * 1.05
@pytest.mark.unit
def test_calculate_all_strategies(self):
"""测试计算所有策略"""
service = PricingService(None)
suggestions = service.calculate_all_strategies(
base_cost=100.0,
target_margin=50.0,
)
assert suggestions.traffic is not None
assert suggestions.profit is not None
assert suggestions.premium is not None
# 价格应该递增:引流款 < 利润款 < 高端款
assert suggestions.traffic.suggested_price < suggestions.profit.suggested_price
assert suggestions.profit.suggested_price < suggestions.premium.suggested_price
@pytest.mark.unit
def test_calculate_all_strategies_selected(self):
"""测试只计算选定的策略"""
service = PricingService(None)
suggestions = service.calculate_all_strategies(
base_cost=100.0,
target_margin=50.0,
strategies=[StrategyType.TRAFFIC, StrategyType.PROFIT],
)
assert suggestions.traffic is not None
assert suggestions.profit is not None
assert suggestions.premium is None
@pytest.mark.unit
@pytest.mark.asyncio
async def test_get_project_with_cost(
self,
db_session: AsyncSession,
sample_project_with_costs: Project
):
"""测试获取项目及成本"""
service = PricingService(db_session)
project, cost_summary = await service.get_project_with_cost(
sample_project_with_costs.id
)
assert project.id == sample_project_with_costs.id
assert project.project_name == "光子嫩肤"
@pytest.mark.unit
@pytest.mark.asyncio
async def test_get_project_with_cost_not_found(
self,
db_session: AsyncSession
):
"""测试项目不存在时的错误处理"""
service = PricingService(db_session)
with pytest.raises(ValueError, match="项目不存在"):
await service.get_project_with_cost(99999)
@pytest.mark.unit
@pytest.mark.asyncio
async def test_create_pricing_plan(
self,
db_session: AsyncSession,
sample_project: Project
):
"""测试创建定价方案"""
# 先添加成本汇总
cost_summary = ProjectCostSummary(
project_id=sample_project.id,
material_cost=Decimal("40.00"),
equipment_cost=Decimal("50.00"),
labor_cost=Decimal("60.00"),
fixed_cost_allocation=Decimal("30.00"),
total_cost=Decimal("180.00"),
calculated_at=datetime.now(),
)
db_session.add(cost_summary)
await db_session.commit()
service = PricingService(db_session)
plan = await service.create_pricing_plan(
project_id=sample_project.id,
plan_name="测试定价方案",
strategy_type=StrategyType.PROFIT,
target_margin=50.0,
)
assert plan.project_id == sample_project.id
assert plan.plan_name == "测试定价方案"
assert plan.strategy_type == "profit"
assert float(plan.target_margin) == 50.0
assert float(plan.base_cost) == 180.0
assert plan.suggested_price > plan.base_cost
@pytest.mark.unit
@pytest.mark.asyncio
async def test_update_pricing_plan(
self,
db_session: AsyncSession,
sample_pricing_plan: PricingPlan
):
"""测试更新定价方案"""
service = PricingService(db_session)
updated = await service.update_pricing_plan(
plan_id=sample_pricing_plan.id,
final_price=599.00,
plan_name="更新后方案名",
)
assert float(updated.final_price) == 599.00
assert updated.plan_name == "更新后方案名"
@pytest.mark.unit
@pytest.mark.asyncio
async def test_update_pricing_plan_not_found(
self,
db_session: AsyncSession
):
"""测试更新不存在的方案"""
service = PricingService(db_session)
with pytest.raises(ValueError, match="定价方案不存在"):
await service.update_pricing_plan(
plan_id=99999,
final_price=599.00,
)
@pytest.mark.unit
@pytest.mark.asyncio
async def test_simulate_strategies(
self,
db_session: AsyncSession,
sample_project: Project
):
"""测试策略模拟"""
# 添加成本汇总
cost_summary = ProjectCostSummary(
project_id=sample_project.id,
total_cost=Decimal("200.00"),
material_cost=Decimal("100.00"),
equipment_cost=Decimal("50.00"),
labor_cost=Decimal("50.00"),
fixed_cost_allocation=Decimal("0.00"),
calculated_at=datetime.now(),
)
db_session.add(cost_summary)
await db_session.commit()
service = PricingService(db_session)
response = await service.simulate_strategies(
project_id=sample_project.id,
strategies=[StrategyType.TRAFFIC, StrategyType.PROFIT, StrategyType.PREMIUM],
target_margin=50.0,
)
assert response.project_id == sample_project.id
assert response.base_cost == 200.0
assert len(response.results) == 3
# 验证结果排序
prices = [r.suggested_price for r in response.results]
assert prices == sorted(prices) # 应该是升序
@pytest.mark.unit
def test_format_cost_data(self):
"""测试成本数据格式化"""
service = PricingService(None)
# 测试空数据
result = service._format_cost_data(None)
assert "暂无成本数据" in result
@pytest.mark.unit
def test_format_market_data(self):
"""测试市场数据格式化"""
service = PricingService(None)
# 测试空数据
result = service._format_market_data(None)
assert "暂无市场行情数据" in result
# 测试有数据
market_ref = MarketReference(min=100.0, max=500.0, avg=300.0)
result = service._format_market_data(market_ref)
assert "100.00" in result
assert "500.00" in result
assert "300.00" in result
@pytest.mark.unit
def test_extract_recommendations(self):
"""测试提取 AI 建议列表"""
service = PricingService(None)
content = """
根据分析,建议如下:
- 建议一:常规定价 580 元
- 建议二:新客首单 388 元
* 建议三VIP 会员 520 元
1. 定期促销活动
2. 会员体系建设
"""
recommendations = service._extract_recommendations(content)
assert len(recommendations) == 5
assert "常规定价" in recommendations[0]
class TestPricingServiceWithAI:
"""需要 AI 服务的定价测试"""
@pytest.mark.unit
@pytest.mark.asyncio
@patch('app.services.pricing_service.AIServiceWrapper')
async def test_generate_pricing_advice_ai_failure(
self,
mock_ai_wrapper,
db_session: AsyncSession,
sample_project: Project
):
"""测试 AI 调用失败时的降级处理"""
# 添加成本汇总
cost_summary = ProjectCostSummary(
project_id=sample_project.id,
total_cost=Decimal("200.00"),
material_cost=Decimal("100.00"),
equipment_cost=Decimal("50.00"),
labor_cost=Decimal("50.00"),
fixed_cost_allocation=Decimal("0.00"),
calculated_at=datetime.now(),
)
db_session.add(cost_summary)
await db_session.commit()
# 模拟 AI 调用失败
mock_instance = MagicMock()
mock_instance.chat = AsyncMock(side_effect=Exception("AI 服务不可用"))
mock_ai_wrapper.return_value = mock_instance
service = PricingService(db_session)
# 即使 AI 失败,基本定价计算应该仍然返回
response = await service.generate_pricing_advice(
project_id=sample_project.id,
target_margin=50.0,
)
# 验证基本定价仍然可用
assert response.project_id == sample_project.id
assert response.cost_base == 200.0
assert response.pricing_suggestions is not None
# AI 建议可能为空
assert response.ai_advice is None or response.ai_usage is None