179 lines
6.4 KiB
Python
179 lines
6.4 KiB
Python
"""工具层测试:cost_engine, predict_engine, cache 核心函数"""
|
||
import pytest
|
||
from app.utils.cost_engine import (
|
||
calc_variance,
|
||
calc_efficiency_variance,
|
||
)
|
||
from app.utils.predict_engine import (
|
||
cvp_analysis,
|
||
npv,
|
||
irr,
|
||
sensitivity_analysis,
|
||
scenario_analysis,
|
||
)
|
||
from app.utils.cache import _make_key, get, set, delete, clear_all
|
||
|
||
|
||
class TestCostVariance:
|
||
"""成本差异计算测试"""
|
||
|
||
def test_calc_variance_standard(self):
|
||
"""标准差异计算"""
|
||
result = calc_variance(10, 12, 5, 4.5)
|
||
# 量差 = (12-10)*5 = 10
|
||
# 价差 = (4.5-5)*12 = -6
|
||
# 总差异 = 10 + (-6) = 4
|
||
assert result["qty_variance"] == 10.0
|
||
assert result["price_variance"] == -6.0
|
||
assert result["total_variance"] == 4.0
|
||
assert result["standard_cost"] == 50.0
|
||
assert result["actual_cost"] == 54.0
|
||
|
||
def test_calc_variance_equal(self):
|
||
"""实际与标准完全一致"""
|
||
result = calc_variance(10, 10, 5, 5)
|
||
assert result["qty_variance"] == 0
|
||
assert result["price_variance"] == 0
|
||
assert result["total_variance"] == 0
|
||
|
||
def test_calc_variance_no_qty_diff(self):
|
||
"""只有价格差异"""
|
||
result = calc_variance(10, 10, 5, 6)
|
||
assert result["qty_variance"] == 0
|
||
assert result["price_variance"] == 10.0 # (6-5)*10
|
||
assert result["total_variance"] == 10.0
|
||
|
||
def test_calc_efficiency_variance(self):
|
||
"""效率差异计算"""
|
||
result = calc_efficiency_variance(100, 120, 50)
|
||
# (120-100)*50 = 1000
|
||
assert result["efficiency_variance"] == 1000.0
|
||
|
||
|
||
class TestCvpAnalysis:
|
||
"""CVP本量利分析测试"""
|
||
|
||
def test_basic_bep(self):
|
||
"""基础盈亏平衡点"""
|
||
result = cvp_analysis(unit_price=100, unit_variable_cost=60, fixed_cost=20000)
|
||
assert result["contribution_margin"] == 40.0
|
||
assert result["contribution_ratio"] == 40.0
|
||
assert result["bep_units"] == 500.0
|
||
assert result["bep_revenue"] == 50000.0
|
||
|
||
def test_with_actual_volume(self):
|
||
"""带实际销量的安全边际计算"""
|
||
result = cvp_analysis(unit_price=100, unit_variable_cost=60, fixed_cost=20000, actual_volume=800)
|
||
assert result["safety_margin_units"] == 300.0 # 800-500
|
||
assert result["safety_margin_ratio"] == 37.5 # 300/800*100
|
||
assert result["actual_profit"] == 12000.0 # (100-60)*800 - 20000
|
||
|
||
def test_with_target_profit(self):
|
||
"""带目标利润"""
|
||
result = cvp_analysis(unit_price=100, unit_variable_cost=60, fixed_cost=20000, target_profit=10000)
|
||
assert result["target_units"] == 750.0 # (20000+10000)/40
|
||
assert result["target_revenue"] == 75000.0
|
||
|
||
def test_price_less_than_variable_cost(self):
|
||
"""单价<=变动成本 → 错误"""
|
||
result = cvp_analysis(unit_price=50, unit_variable_cost=60, fixed_cost=10000)
|
||
assert "error" in result
|
||
|
||
|
||
class TestNpvIrr:
|
||
"""NPV/IRR投资决策测试"""
|
||
|
||
def test_npv_positive(self):
|
||
"""正NPV → 可行"""
|
||
result = npv(1000, [400, 500, 500], 10)
|
||
assert result["is_viable"] is True
|
||
assert result["npv"] > 0
|
||
assert result["profitability_index"] > 1.0
|
||
|
||
def test_npv_negative(self):
|
||
"""负NPV → 不可行"""
|
||
result = npv(10000, [100, 100, 100], 10)
|
||
assert result["is_viable"] is False
|
||
assert result["npv"] < 0
|
||
|
||
def test_npv_empty_cashflows(self):
|
||
"""空现金流 → 错误"""
|
||
result = npv(1000, [], 10)
|
||
assert "error" in result
|
||
|
||
def test_irr_basic(self):
|
||
"""基本IRR计算"""
|
||
result = irr(1000, [300, 400, 500, 200])
|
||
assert result["payback_period"] is not None
|
||
assert result["is_viable"] is True # IRR > 0
|
||
|
||
def test_irr_negative_case(self):
|
||
"""现金流总和小于投资→可能负IRR"""
|
||
result = irr(10000, [1000, 1000, 1000])
|
||
assert result["payback_period"] is None # 无法回收
|
||
|
||
|
||
class TestSensitivityAnalysis:
|
||
"""敏感性分析测试"""
|
||
|
||
def test_basic_sensitivity(self):
|
||
"""基础敏感性分析"""
|
||
result = sensitivity_analysis(base_revenue=1000, base_cost=800, step=10, max_step=20)
|
||
assert result["base_profit"] == 200.0
|
||
assert len(result["factors"]) > 0
|
||
# 0%变动时利润不变
|
||
zero = [f for f in result["factors"] if f["change_pct"] == 0][0]
|
||
assert zero["revenue_change_profit"] == 200.0
|
||
assert zero["revenue_sensitivity"] == 0.0
|
||
|
||
|
||
class TestScenarioAnalysis:
|
||
"""情景模拟测试"""
|
||
|
||
def test_three_scenarios(self):
|
||
"""三情景分析"""
|
||
result = scenario_analysis(
|
||
optimistic={"revenue": 130, "cost": 90},
|
||
pessimistic={"revenue": 80, "cost": 110},
|
||
base={"revenue": 100, "cost": 100},
|
||
)
|
||
assert len(result["scenarios"]) == 3
|
||
assert result["scenarios"][0]["scenario"] == "乐观"
|
||
assert result["scenarios"][1]["scenario"] == "中性"
|
||
assert result["scenarios"][2]["scenario"] == "悲观"
|
||
assert result["expected_profit"] == round((40 + 0 + (-30)) / 3, 2)
|
||
|
||
|
||
class TestCacheUtils:
|
||
"""缓存工具测试(不依赖Redis,测试逻辑)"""
|
||
|
||
def test_make_key_format(self):
|
||
"""缓存key格式"""
|
||
key = _make_key("test", "hello")
|
||
assert key.startswith("cma:cache:test:")
|
||
assert len(key) > len("cma:cache:test:")
|
||
|
||
def test_get_when_unavailable(self):
|
||
"""Redis不可用 → get返回None"""
|
||
# cache.py 在模块级别会尝试连接Redis,如果不可用则 _available=False
|
||
# 此时 get 返回 None
|
||
result = get("test", "key")
|
||
# 可能返回 None(Redis不可用)或者正常获取
|
||
assert result is None or isinstance(result, object)
|
||
|
||
def test_set_when_unavailable(self):
|
||
"""Redis不可用 → set返回False"""
|
||
result = set("test", "key", {"data": 123})
|
||
# 可能返回 False(Redis不可用)或者 True
|
||
assert result is False or result is True
|
||
|
||
def test_delete_when_unavailable(self):
|
||
"""Redis不可用 → delete返回False"""
|
||
result = delete("test", "key")
|
||
assert result is False or result is True
|
||
|
||
def test_clear_all_when_unavailable(self):
|
||
"""Redis不可用 → clear_all返回False"""
|
||
result = clear_all()
|
||
assert result is False or result is True
|