add backend source code
This commit is contained in:
296
backend/tests/test_dimension_weight_service.py
Normal file
296
backend/tests/test_dimension_weight_service.py
Normal file
@@ -0,0 +1,296 @@
|
||||
"""
|
||||
科室类型BSC维度权重配置服务测试用例
|
||||
|
||||
测试覆盖:
|
||||
1. 权重配置获取
|
||||
2. 权重配置创建/更新
|
||||
3. 维度加权得分计算
|
||||
4. 默认权重初始化
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from decimal import Decimal
|
||||
|
||||
from app.services.dimension_weight_service import DimensionWeightService, DEFAULT_WEIGHTS
|
||||
from app.models.models import DeptType, DeptTypeDimensionWeight
|
||||
|
||||
|
||||
class TestGetDimensionWeights:
|
||||
"""获取权重配置测试"""
|
||||
|
||||
def test_get_default_weights_surgical(self):
|
||||
"""测试获取手术临床科室默认权重"""
|
||||
weights = DimensionWeightService.get_dimension_weights(DeptType.CLINICAL_SURGICAL)
|
||||
|
||||
assert weights["financial"] == 0.60
|
||||
assert weights["customer"] == 0.15
|
||||
assert weights["internal_process"] == 0.20
|
||||
assert weights["learning_growth"] == 0.05
|
||||
|
||||
def test_get_default_weights_medical_tech(self):
|
||||
"""测试获取医技科室默认权重"""
|
||||
weights = DimensionWeightService.get_dimension_weights(DeptType.MEDICAL_TECH)
|
||||
|
||||
assert weights["financial"] == 0.40
|
||||
assert weights["customer"] == 0.25
|
||||
assert weights["internal_process"] == 0.30
|
||||
assert weights["learning_growth"] == 0.05
|
||||
|
||||
def test_get_default_weights_nursing(self):
|
||||
"""测试获取护理单元默认权重"""
|
||||
weights = DimensionWeightService.get_dimension_weights(DeptType.NURSING)
|
||||
|
||||
assert weights["financial"] == 0.20
|
||||
assert weights["customer"] == 0.15
|
||||
assert weights["internal_process"] == 0.50
|
||||
assert weights["learning_growth"] == 0.15
|
||||
|
||||
def test_get_default_weights_unknown_type(self):
|
||||
"""测试获取未知类型的默认权重"""
|
||||
weights = DimensionWeightService.get_dimension_weights("unknown_type")
|
||||
|
||||
# 应返回通用默认值
|
||||
assert "financial" in weights
|
||||
assert "customer" in weights
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_dept_type(self):
|
||||
"""测试根据科室类型获取数据库配置"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
# 创建模拟配置
|
||||
config = MagicMock()
|
||||
config.dept_type = DeptType.CLINICAL_SURGICAL
|
||||
config.financial_weight = 0.60
|
||||
config.customer_weight = 0.15
|
||||
config.internal_process_weight = 0.20
|
||||
config.learning_growth_weight = 0.05
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = config
|
||||
|
||||
with patch('app.services.dimension_weight_service.select') as mock_select:
|
||||
mock_db.execute.return_value = mock_result
|
||||
|
||||
result = await DimensionWeightService.get_by_dept_type(
|
||||
mock_db, DeptType.CLINICAL_SURGICAL
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
|
||||
|
||||
class TestCreateUpdateWeights:
|
||||
"""创建/更新权重配置测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_weight_config(self):
|
||||
"""测试创建权重配置"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
# 模拟不存在现有配置
|
||||
with patch.object(DimensionWeightService, 'get_by_dept_type', return_value=None):
|
||||
result = await DimensionWeightService.create_or_update(
|
||||
mock_db,
|
||||
dept_type=DeptType.CLINICAL_SURGICAL,
|
||||
financial_weight=0.60,
|
||||
customer_weight=0.15,
|
||||
internal_process_weight=0.20,
|
||||
learning_growth_weight=0.05,
|
||||
description="手术临床科室"
|
||||
)
|
||||
|
||||
mock_db.add.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_weight_config(self):
|
||||
"""测试更新权重配置"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
# 创建模拟现有配置
|
||||
existing = MagicMock()
|
||||
existing.dept_type = DeptType.CLINICAL_SURGICAL
|
||||
existing.financial_weight = 0.50
|
||||
existing.customer_weight = 0.20
|
||||
|
||||
with patch.object(DimensionWeightService, 'get_by_dept_type', return_value=existing):
|
||||
result = await DimensionWeightService.create_or_update(
|
||||
mock_db,
|
||||
dept_type=DeptType.CLINICAL_SURGICAL,
|
||||
financial_weight=0.60,
|
||||
customer_weight=0.15,
|
||||
internal_process_weight=0.20,
|
||||
learning_growth_weight=0.05
|
||||
)
|
||||
|
||||
assert existing.financial_weight == 0.60
|
||||
assert existing.customer_weight == 0.15
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_weight_config_invalid_total(self):
|
||||
"""测试创建权重配置总和不为100%"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
with patch.object(DimensionWeightService, 'get_by_dept_type', return_value=None):
|
||||
with pytest.raises(ValueError, match="权重总和必须为100%"):
|
||||
await DimensionWeightService.create_or_update(
|
||||
mock_db,
|
||||
dept_type=DeptType.CLINICAL_SURGICAL,
|
||||
financial_weight=0.50, # 50%
|
||||
customer_weight=0.30, # 30%
|
||||
internal_process_weight=0.10, # 10%
|
||||
learning_growth_weight=0.05 # 5%
|
||||
# 总计: 95% != 100%
|
||||
)
|
||||
|
||||
|
||||
class TestCalculateWeightedScore:
|
||||
"""维度加权得分计算测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calculate_dimension_weighted_score_surgical(self):
|
||||
"""测试手术临床科室维度加权得分计算"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
# 模拟数据库返回权重配置
|
||||
config = MagicMock()
|
||||
config.financial_weight = Decimal("0.60")
|
||||
config.customer_weight = Decimal("0.15")
|
||||
config.internal_process_weight = Decimal("0.20")
|
||||
config.learning_growth_weight = Decimal("0.05")
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = config
|
||||
|
||||
with patch.object(DimensionWeightService, 'get_by_dept_type', return_value=config):
|
||||
result = await DimensionWeightService.calculate_dimension_weighted_score(
|
||||
mock_db,
|
||||
dept_type=DeptType.CLINICAL_SURGICAL,
|
||||
financial_score=90,
|
||||
customer_score=85,
|
||||
internal_process_score=88,
|
||||
learning_growth_score=92
|
||||
)
|
||||
|
||||
# 验证结果
|
||||
assert "weights" in result
|
||||
assert "weighted_scores" in result
|
||||
assert "total_score" in result
|
||||
|
||||
# 计算总分
|
||||
# 财务: 90 * 0.60 = 54
|
||||
# 客户: 85 * 0.15 = 12.75
|
||||
# 流程: 88 * 0.20 = 17.6
|
||||
# 学习: 92 * 0.05 = 4.6
|
||||
# 总分: 54 + 12.75 + 17.6 + 4.6 = 88.95
|
||||
assert result["total_score"] == 88.95
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calculate_dimension_weighted_score_nursing(self):
|
||||
"""测试护理单元维度加权得分计算"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
config = MagicMock()
|
||||
config.financial_weight = Decimal("0.20")
|
||||
config.customer_weight = Decimal("0.15")
|
||||
config.internal_process_weight = Decimal("0.50")
|
||||
config.learning_growth_weight = Decimal("0.15")
|
||||
|
||||
with patch.object(DimensionWeightService, 'get_by_dept_type', return_value=config):
|
||||
result = await DimensionWeightService.calculate_dimension_weighted_score(
|
||||
mock_db,
|
||||
dept_type=DeptType.NURSING,
|
||||
financial_score=80,
|
||||
customer_score=90,
|
||||
internal_process_score=85,
|
||||
learning_growth_score=88
|
||||
)
|
||||
|
||||
# 护理单元的内部流程权重最高(50%)
|
||||
assert result["weights"]["internal_process"] == 0.50
|
||||
assert result["weighted_scores"]["internal_process"] == 42.5 # 85 * 0.50
|
||||
|
||||
|
||||
class TestInitDefaultWeights:
|
||||
"""默认权重初始化测试"""
|
||||
|
||||
def test_default_weights_completeness(self):
|
||||
"""测试默认权重覆盖所有科室类型"""
|
||||
expected_types = [
|
||||
DeptType.CLINICAL_SURGICAL,
|
||||
DeptType.CLINICAL_NONSURGICAL_WARD,
|
||||
DeptType.CLINICAL_NONSURGICAL_NOWARD,
|
||||
DeptType.MEDICAL_TECH,
|
||||
DeptType.MEDICAL_AUXILIARY,
|
||||
DeptType.NURSING,
|
||||
DeptType.ADMIN,
|
||||
DeptType.FINANCE,
|
||||
DeptType.LOGISTICS,
|
||||
]
|
||||
|
||||
for dept_type in expected_types:
|
||||
assert dept_type in DEFAULT_WEIGHTS, f"缺少科室类型 {dept_type} 的默认权重"
|
||||
|
||||
def test_default_weights_sum_to_one(self):
|
||||
"""测试所有默认权重总和为1"""
|
||||
for dept_type, weights in DEFAULT_WEIGHTS.items():
|
||||
total = (
|
||||
weights["financial"] +
|
||||
weights["customer"] +
|
||||
weights["internal_process"] +
|
||||
weights["learning_growth"]
|
||||
)
|
||||
assert abs(total - 1.0) < 0.01, f"{dept_type} 权重总和为 {total},不为 1.0"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_init_default_weights(self):
|
||||
"""测试初始化默认权重"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
# 模拟不存在现有配置
|
||||
with patch.object(DimensionWeightService, 'get_by_dept_type', return_value=None):
|
||||
with patch.object(DimensionWeightService, 'create_or_update') as mock_create:
|
||||
mock_create.return_value = MagicMock()
|
||||
|
||||
result = await DimensionWeightService.init_default_weights(mock_db)
|
||||
|
||||
# 应该为每个科室类型创建配置
|
||||
assert len(result) == len(DEFAULT_WEIGHTS)
|
||||
|
||||
|
||||
class TestDeleteWeight:
|
||||
"""删除权重配置测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_weight_config(self):
|
||||
"""测试删除权重配置(软删除)"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
config = MagicMock()
|
||||
config.id = 1
|
||||
config.is_active = True
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = config
|
||||
|
||||
with patch('app.services.dimension_weight_service.select') as mock_select:
|
||||
mock_db.execute.return_value = mock_result
|
||||
|
||||
result = await DimensionWeightService.delete(mock_db, 1)
|
||||
|
||||
assert result is True
|
||||
assert config.is_active is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_nonexistent_config(self):
|
||||
"""测试删除不存在的配置"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = None
|
||||
|
||||
with patch('app.services.dimension_weight_service.select') as mock_select:
|
||||
mock_db.execute.return_value = mock_result
|
||||
|
||||
result = await DimensionWeightService.delete(mock_db, 999)
|
||||
|
||||
assert result is False
|
||||
535
backend/tests/test_e2e.py
Normal file
535
backend/tests/test_e2e.py
Normal file
@@ -0,0 +1,535 @@
|
||||
"""
|
||||
完整闭环测试用例 - End-to-End Tests
|
||||
|
||||
测试覆盖完整的业务流程:
|
||||
1. 用户登录 -> 获取Token
|
||||
2. 科室管理 -> CRUD操作
|
||||
3. 员工管理 -> CRUD操作
|
||||
4. 考核指标管理 -> CRUD操作
|
||||
5. 考核流程 -> 创建、提交、审核、确认
|
||||
6. 满意度调查 -> 创建问卷、提交回答、统计
|
||||
7. 评分方法计算 -> 各种评分方法验证
|
||||
8. BSC维度权重 -> 配置和计算
|
||||
"""
|
||||
import pytest
|
||||
import httpx
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
# API基础URL
|
||||
BASE_URL = "http://localhost:8000/api/v1"
|
||||
|
||||
|
||||
class TestAuth:
|
||||
"""认证测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_success(self):
|
||||
"""测试登录成功"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{BASE_URL}/auth/login-json",
|
||||
json={"username": "admin", "password": "admin123"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
assert "access_token" in data["data"]
|
||||
return data["data"]["access_token"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_failure(self):
|
||||
"""测试登录失败"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{BASE_URL}/auth/login-json",
|
||||
json={"username": "admin", "password": "wrong_password"}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestDepartmentManagement:
|
||||
"""科室管理闭环测试"""
|
||||
|
||||
@pytest.fixture
|
||||
async def auth_token(self):
|
||||
"""获取认证Token"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{BASE_URL}/auth/login-json",
|
||||
json={"username": "admin", "password": "admin123"}
|
||||
)
|
||||
return response.json()["data"]["access_token"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_department_list(self, auth_token):
|
||||
"""测试获取科室列表"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{BASE_URL}/departments",
|
||||
headers={"Authorization": f"Bearer {auth_token}"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
assert isinstance(data["data"], list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_and_delete_department(self, auth_token):
|
||||
"""测试创建和删除科室"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
headers = {"Authorization": f"Bearer {auth_token}"}
|
||||
|
||||
# 创建科室
|
||||
create_response = await client.post(
|
||||
f"{BASE_URL}/departments",
|
||||
headers=headers,
|
||||
json={
|
||||
"name": "测试科室E2E",
|
||||
"code": "TEST_E2E_001",
|
||||
"dept_type": "clinical_surgical",
|
||||
"description": "E2E测试用科室"
|
||||
}
|
||||
)
|
||||
|
||||
assert create_response.status_code == 200
|
||||
created = create_response.json()
|
||||
assert created["code"] == 200
|
||||
dept_id = created["data"]["id"]
|
||||
|
||||
# 获取科室详情
|
||||
get_response = await client.get(
|
||||
f"{BASE_URL}/departments/{dept_id}",
|
||||
headers=headers
|
||||
)
|
||||
assert get_response.status_code == 200
|
||||
|
||||
# 删除科室
|
||||
delete_response = await client.delete(
|
||||
f"{BASE_URL}/departments/{dept_id}",
|
||||
headers=headers
|
||||
)
|
||||
assert delete_response.status_code == 200
|
||||
|
||||
|
||||
class TestStaffManagement:
|
||||
"""员工管理闭环测试"""
|
||||
|
||||
@pytest.fixture
|
||||
async def auth_token(self):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{BASE_URL}/auth/login",
|
||||
json={"username": "admin", "password": "admin123"}
|
||||
)
|
||||
return response.json()["data"]["access_token"]
|
||||
|
||||
@pytest.fixture
|
||||
async def department_id(self, auth_token):
|
||||
"""获取一个科室ID用于测试"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{BASE_URL}/departments",
|
||||
headers={"Authorization": f"Bearer {auth_token}"}
|
||||
)
|
||||
departments = response.json()["data"]
|
||||
if departments:
|
||||
return departments[0]["id"]
|
||||
return None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_staff_list(self, auth_token):
|
||||
"""测试获取员工列表"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{BASE_URL}/staff",
|
||||
headers={"Authorization": f"Bearer {auth_token}"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
|
||||
|
||||
class TestIndicatorManagement:
|
||||
"""考核指标管理闭环测试"""
|
||||
|
||||
@pytest.fixture
|
||||
async def auth_token(self):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{BASE_URL}/auth/login",
|
||||
json={"username": "admin", "password": "admin123"}
|
||||
)
|
||||
return response.json()["data"]["access_token"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_indicator_list(self, auth_token):
|
||||
"""测试获取指标列表"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{BASE_URL}/indicators",
|
||||
headers={"Authorization": f"Bearer {auth_token}"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_indicator(self, auth_token):
|
||||
"""测试创建指标"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{BASE_URL}/indicators",
|
||||
headers={"Authorization": f"Bearer {auth_token}"},
|
||||
json={
|
||||
"name": "E2E测试指标",
|
||||
"code": "E2E_TEST_001",
|
||||
"indicator_type": "quality",
|
||||
"bs_dimension": "internal_process",
|
||||
"weight": 1.0,
|
||||
"max_score": 100,
|
||||
"target_value": 95,
|
||||
"target_unit": "%",
|
||||
"calculation_method": "测试计算方法",
|
||||
"assessment_method": "interval_high"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
|
||||
# 清理:删除创建的指标
|
||||
indicator_id = data["data"]["id"]
|
||||
async with httpx.AsyncClient() as client:
|
||||
await client.delete(
|
||||
f"{BASE_URL}/indicators/{indicator_id}",
|
||||
headers={"Authorization": f"Bearer {auth_token}"}
|
||||
)
|
||||
|
||||
|
||||
class TestAssessmentWorkflow:
|
||||
"""考核流程闭环测试"""
|
||||
|
||||
@pytest.fixture
|
||||
async def auth_token(self):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{BASE_URL}/auth/login",
|
||||
json={"username": "admin", "password": "admin123"}
|
||||
)
|
||||
return response.json()["data"]["access_token"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_assessment_list(self, auth_token):
|
||||
"""测试获取考核列表"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{BASE_URL}/assessments",
|
||||
headers={"Authorization": f"Bearer {auth_token}"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
|
||||
|
||||
class TestSurveyManagement:
|
||||
"""满意度调查闭环测试"""
|
||||
|
||||
@pytest.fixture
|
||||
async def auth_token(self):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{BASE_URL}/auth/login",
|
||||
json={"username": "admin", "password": "admin123"}
|
||||
)
|
||||
return response.json()["data"]["access_token"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_survey_list(self, auth_token):
|
||||
"""测试获取问卷列表"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{BASE_URL}/surveys",
|
||||
headers={"Authorization": f"Bearer {auth_token}"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
assert isinstance(data["data"], list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_survey_detail(self, auth_token):
|
||||
"""测试获取问卷详情"""
|
||||
# 先获取问卷列表
|
||||
async with httpx.AsyncClient() as client:
|
||||
list_response = await client.get(
|
||||
f"{BASE_URL}/surveys",
|
||||
headers={"Authorization": f"Bearer {auth_token}"}
|
||||
)
|
||||
surveys = list_response.json()["data"]
|
||||
|
||||
if surveys:
|
||||
survey_id = surveys[0]["id"]
|
||||
detail_response = await client.get(
|
||||
f"{BASE_URL}/surveys/{survey_id}",
|
||||
headers={"Authorization": f"Bearer {auth_token}"}
|
||||
)
|
||||
assert detail_response.status_code == 200
|
||||
data = detail_response.json()
|
||||
assert data["code"] == 200
|
||||
assert data["data"]["id"] == survey_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_and_close_survey(self, auth_token):
|
||||
"""测试创建和关闭问卷"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
headers = {"Authorization": f"Bearer {auth_token}"}
|
||||
|
||||
# 创建问卷
|
||||
create_response = await client.post(
|
||||
f"{BASE_URL}/surveys",
|
||||
headers=headers,
|
||||
json={
|
||||
"survey_name": "E2E测试问卷",
|
||||
"survey_code": "E2E_TEST_SURVEY_001",
|
||||
"survey_type": "inpatient",
|
||||
"description": "E2E测试用问卷",
|
||||
"is_anonymous": True,
|
||||
"questions": [
|
||||
{
|
||||
"question_text": "您对服务是否满意?",
|
||||
"question_type": "score",
|
||||
"score_max": 5,
|
||||
"is_required": True
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
assert create_response.status_code == 200
|
||||
created = create_response.json()
|
||||
assert created["code"] == 200
|
||||
survey_id = created["data"]["id"]
|
||||
|
||||
# 发布问卷
|
||||
publish_response = await client.post(
|
||||
f"{BASE_URL}/surveys/{survey_id}/publish",
|
||||
headers=headers
|
||||
)
|
||||
assert publish_response.status_code == 200
|
||||
|
||||
# 关闭问卷
|
||||
close_response = await client.post(
|
||||
f"{BASE_URL}/surveys/{survey_id}/close",
|
||||
headers=headers
|
||||
)
|
||||
assert close_response.status_code == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_department_satisfaction(self, auth_token):
|
||||
"""测试获取科室满意度统计"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{BASE_URL}/surveys/stats/department",
|
||||
headers={"Authorization": f"Bearer {auth_token}"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
|
||||
|
||||
class TestScoringMethodIntegration:
|
||||
"""评分方法集成测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_target_reference_method(self):
|
||||
"""测试目标参照法(单元测试集成)"""
|
||||
from app.services.scoring_service import ScoringService, ScoringMethod, ScoringParams
|
||||
|
||||
params = ScoringParams(weight=12.6, target_value=15.0)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.TARGET_REFERENCE,
|
||||
actual_value=18.0,
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 15.12 # 12.6 * (18/15)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_interval_high_method(self):
|
||||
"""测试区间法-趋高指标"""
|
||||
from app.services.scoring_service import ScoringService, ScoringMethod, ScoringParams
|
||||
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
best_value=100,
|
||||
baseline_value=80,
|
||||
worst_value=0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.INTERVAL_HIGH,
|
||||
actual_value=100,
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 12.6 # 达到最佳值,满分
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_interval_low_method(self):
|
||||
"""测试区间法-趋低指标"""
|
||||
from app.services.scoring_service import ScoringService, ScoringMethod, ScoringParams
|
||||
|
||||
params = ScoringParams(weight=12.6, target_value=25.0)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.INTERVAL_LOW,
|
||||
actual_value=30.0,
|
||||
params=params
|
||||
)
|
||||
|
||||
# 12.6 * (25/30) = 10.5
|
||||
assert result.score == 10.5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deduction_method(self):
|
||||
"""测试扣分法"""
|
||||
from app.services.scoring_service import ScoringService, ScoringMethod, ScoringParams
|
||||
|
||||
params = ScoringParams(weight=3.2, deduction_per_unit=5.0)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.DEDUCTION,
|
||||
actual_value=1, # 1次违规
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 0 # 3.2 - 5 = -1.8 -> 0 (不计负分)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bonus_method(self):
|
||||
"""测试加分法"""
|
||||
from app.services.scoring_service import ScoringService, ScoringMethod, ScoringParams
|
||||
|
||||
params = ScoringParams(
|
||||
weight=4.5,
|
||||
bonus_per_unit=2.0,
|
||||
max_bonus_ratio=0.5
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.BONUS,
|
||||
actual_value=2, # 2项加分
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 8.5 # 4.5 + 2*2 = 8.5
|
||||
|
||||
|
||||
class TestDimensionWeight:
|
||||
"""BSC维度权重测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_weights(self):
|
||||
"""测试默认权重配置"""
|
||||
from app.services.dimension_weight_service import DimensionWeightService, DEFAULT_WEIGHTS
|
||||
from app.models.models import DeptType
|
||||
|
||||
# 测试手术临床科室权重
|
||||
weights = DimensionWeightService.get_dimension_weights(DeptType.CLINICAL_SURGICAL)
|
||||
assert weights["financial"] == 0.60
|
||||
assert weights["customer"] == 0.15
|
||||
assert weights["internal_process"] == 0.20
|
||||
assert weights["learning_growth"] == 0.05
|
||||
|
||||
# 测试护理单元权重
|
||||
weights = DimensionWeightService.get_dimension_weights(DeptType.NURSING)
|
||||
assert weights["financial"] == 0.20
|
||||
assert weights["internal_process"] == 0.50 # 护理单元内部流程权重最高
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_weight_sum_to_one(self):
|
||||
"""测试所有权重总和为1"""
|
||||
from app.services.dimension_weight_service import DEFAULT_WEIGHTS
|
||||
|
||||
for dept_type, weights in DEFAULT_WEIGHTS.items():
|
||||
total = (
|
||||
weights["financial"] +
|
||||
weights["customer"] +
|
||||
weights["internal_process"] +
|
||||
weights["learning_growth"]
|
||||
)
|
||||
assert abs(total - 1.0) < 0.01, f"{dept_type} 权重总和不为1"
|
||||
|
||||
|
||||
class TestStatsAPI:
|
||||
"""统计报表API测试"""
|
||||
|
||||
@pytest.fixture
|
||||
async def auth_token(self):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{BASE_URL}/auth/login",
|
||||
json={"username": "admin", "password": "admin123"}
|
||||
)
|
||||
return response.json()["data"]["access_token"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_bsc_dimension_stats(self, auth_token):
|
||||
"""测试获取BSC维度统计"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{BASE_URL}/stats/bsc-dimension",
|
||||
headers={"Authorization": f"Bearer {auth_token}"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_department_stats(self, auth_token):
|
||||
"""测试获取科室统计"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{BASE_URL}/stats/department",
|
||||
headers={"Authorization": f"Bearer {auth_token}"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_ranking_stats(self, auth_token):
|
||||
"""测试获取排名统计"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{BASE_URL}/stats/ranking",
|
||||
headers={"Authorization": f"Bearer {auth_token}"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestHealthCheck:
|
||||
"""健康检查测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_endpoint(self):
|
||||
"""测试健康检查端点"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get("http://localhost:8000/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "healthy"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_docs_available(self):
|
||||
"""测试API文档可访问"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"{BASE_URL}/docs")
|
||||
|
||||
assert response.status_code == 200
|
||||
455
backend/tests/test_scoring_service.py
Normal file
455
backend/tests/test_scoring_service.py
Normal file
@@ -0,0 +1,455 @@
|
||||
"""
|
||||
评分方法计算服务测试用例
|
||||
|
||||
测试覆盖:
|
||||
1. 目标参照法计算
|
||||
2. 区间法-趋高指标计算
|
||||
3. 区间法-趋低指标计算
|
||||
4. 区间法-趋中指标计算
|
||||
5. 扣分法计算
|
||||
6. 加分法计算
|
||||
"""
|
||||
import pytest
|
||||
from app.services.scoring_service import (
|
||||
ScoringService, ScoringMethod, ScoringParams, ScoringResult
|
||||
)
|
||||
|
||||
|
||||
class TestTargetReferenceMethod:
|
||||
"""目标参照法测试"""
|
||||
|
||||
def test_target_reference_above_target(self):
|
||||
"""测试实际值超过目标值"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
target_value=15.0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.TARGET_REFERENCE,
|
||||
actual_value=18.0,
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 15.12 # 12.6 * (18/15)
|
||||
assert result.actual_value == 18.0
|
||||
assert result.method == "target_reference"
|
||||
|
||||
def test_target_reference_below_target(self):
|
||||
"""测试实际值低于目标值"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
target_value=15.0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.TARGET_REFERENCE,
|
||||
actual_value=12.0,
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 10.08 # 12.6 * (12/15)
|
||||
|
||||
def test_target_reference_zero_target(self):
|
||||
"""测试目标值为零的情况"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
target_value=0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.TARGET_REFERENCE,
|
||||
actual_value=18.0,
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 0
|
||||
assert "error" in result.details
|
||||
|
||||
|
||||
class TestIntervalHighMethod:
|
||||
"""区间法-趋高指标测试"""
|
||||
|
||||
def test_interval_high_at_best(self):
|
||||
"""测试达到最佳值"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
best_value=100,
|
||||
baseline_value=80,
|
||||
worst_value=0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.INTERVAL_HIGH,
|
||||
actual_value=100,
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 12.6 # 满分
|
||||
|
||||
def test_interval_high_above_best(self):
|
||||
"""测试超过最佳值"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
best_value=100,
|
||||
baseline_value=80,
|
||||
worst_value=0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.INTERVAL_HIGH,
|
||||
actual_value=120,
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 12.6 # 满分
|
||||
|
||||
def test_interval_high_between_baseline_and_best(self):
|
||||
"""测试在基准值和最佳值之间"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
best_value=100,
|
||||
baseline_value=80,
|
||||
worst_value=0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.INTERVAL_HIGH,
|
||||
actual_value=90,
|
||||
params=params
|
||||
)
|
||||
|
||||
# (90-0)/(100-0) * 12.6 = 11.34
|
||||
assert result.score == 11.34
|
||||
|
||||
def test_interval_high_below_baseline(self):
|
||||
"""测试低于基准值"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
best_value=100,
|
||||
baseline_value=80,
|
||||
worst_value=0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.INTERVAL_HIGH,
|
||||
actual_value=60,
|
||||
params=params
|
||||
)
|
||||
|
||||
# 12.6 * (60/80) * 0.8 = 7.56
|
||||
assert result.score == 7.56
|
||||
|
||||
def test_interval_high_missing_params(self):
|
||||
"""测试缺少参数"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
best_value=None,
|
||||
baseline_value=80
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.INTERVAL_HIGH,
|
||||
actual_value=90,
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 0
|
||||
assert "error" in result.details
|
||||
|
||||
|
||||
class TestIntervalLowMethod:
|
||||
"""区间法-趋低指标测试"""
|
||||
|
||||
def test_interval_low_at_target(self):
|
||||
"""测试达到目标值"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
target_value=25.0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.INTERVAL_LOW,
|
||||
actual_value=25.0,
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 12.6 # 满分
|
||||
|
||||
def test_interval_low_below_target(self):
|
||||
"""测试低于目标值"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
target_value=25.0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.INTERVAL_LOW,
|
||||
actual_value=20.0,
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 12.6 # 满分
|
||||
|
||||
def test_interval_low_above_target(self):
|
||||
"""测试超过目标值"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
target_value=25.0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.INTERVAL_LOW,
|
||||
actual_value=30.0,
|
||||
params=params
|
||||
)
|
||||
|
||||
# 12.6 * (25/30) = 10.5
|
||||
assert result.score == 10.5
|
||||
|
||||
def test_interval_low_significantly_above_target(self):
|
||||
"""测试远超目标值"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
target_value=25.0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.INTERVAL_LOW,
|
||||
actual_value=50.0,
|
||||
params=params
|
||||
)
|
||||
|
||||
# 12.6 * (25/50) = 6.3
|
||||
assert result.score == 6.3
|
||||
|
||||
|
||||
class TestIntervalCenterMethod:
|
||||
"""区间法-趋中指标测试"""
|
||||
|
||||
def test_interval_center_within_deviation(self):
|
||||
"""测试在允许偏差范围内"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
target_value=10.0,
|
||||
allowed_deviation=2.0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.INTERVAL_CENTER,
|
||||
actual_value=11.0,
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 12.6 # 满分
|
||||
|
||||
def test_interval_center_outside_deviation(self):
|
||||
"""测试超出允许偏差"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
target_value=10.0,
|
||||
allowed_deviation=2.0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.INTERVAL_CENTER,
|
||||
actual_value=14.0,
|
||||
params=params
|
||||
)
|
||||
|
||||
# 偏差 = 14-10 = 4, 超出 = 4-2 = 2
|
||||
# 扣分比例 = 2/2 = 1
|
||||
# 得分 = 12.6 * (1-1) = 0
|
||||
assert result.score == 0
|
||||
|
||||
def test_interval_center_partial_deviation(self):
|
||||
"""测试部分超出允许偏差"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
target_value=10.0,
|
||||
allowed_deviation=2.0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.INTERVAL_CENTER,
|
||||
actual_value=12.5,
|
||||
params=params
|
||||
)
|
||||
|
||||
# 偏差 = 12.5-10 = 2.5, 超出 = 2.5-2 = 0.5
|
||||
# 扣分比例 = 0.5/2 = 0.25
|
||||
# 得分 = 12.6 * (1-0.25) = 9.45
|
||||
assert result.score == 9.45
|
||||
|
||||
|
||||
class TestDeductionMethod:
|
||||
"""扣分法测试"""
|
||||
|
||||
def test_deduction_no_occurrence(self):
|
||||
"""测试无违规情况"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
deduction_per_unit=5.0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.DEDUCTION,
|
||||
actual_value=0,
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 12.6 # 满分
|
||||
|
||||
def test_deduction_one_occurrence(self):
|
||||
"""测试一次违规"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
deduction_per_unit=5.0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.DEDUCTION,
|
||||
actual_value=1,
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 7.6 # 12.6 - 5
|
||||
|
||||
def test_deduction_multiple_occurrences(self):
|
||||
"""测试多次违规"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
deduction_per_unit=5.0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.DEDUCTION,
|
||||
actual_value=2,
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 2.6 # 12.6 - 10
|
||||
|
||||
def test_deduction_exhausted(self):
|
||||
"""测试扣分超过满分"""
|
||||
params = ScoringParams(
|
||||
weight=12.6,
|
||||
deduction_per_unit=5.0
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.DEDUCTION,
|
||||
actual_value=3,
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 0 # 不计负分
|
||||
|
||||
|
||||
class TestBonusMethod:
|
||||
"""加分法测试"""
|
||||
|
||||
def test_bonus_no_bonus(self):
|
||||
"""测试无加分项"""
|
||||
params = ScoringParams(
|
||||
weight=4.5,
|
||||
bonus_per_unit=2.0,
|
||||
max_bonus_ratio=0.5
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.BONUS,
|
||||
actual_value=0,
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 4.5 # 基础分
|
||||
|
||||
def test_bonus_one_item(self):
|
||||
"""测试一项加分"""
|
||||
params = ScoringParams(
|
||||
weight=4.5,
|
||||
bonus_per_unit=2.0,
|
||||
max_bonus_ratio=0.5
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.BONUS,
|
||||
actual_value=1,
|
||||
params=params
|
||||
)
|
||||
|
||||
assert result.score == 6.5 # 4.5 + 2
|
||||
|
||||
def test_bonus_multiple_items(self):
|
||||
"""测试多项加分"""
|
||||
params = ScoringParams(
|
||||
weight=4.5,
|
||||
bonus_per_unit=2.0,
|
||||
max_bonus_ratio=0.5
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.BONUS,
|
||||
actual_value=3,
|
||||
params=params
|
||||
)
|
||||
|
||||
# 3项 * 2分 = 6分,但最大加分为权重的50% = 2.25
|
||||
# 所以总分 = 4.5 + 2.25 = 6.75
|
||||
assert result.score == 6.75
|
||||
assert result.details["status"] == "达到加分上限"
|
||||
|
||||
def test_bonus_exceeds_max(self):
|
||||
"""测试加分超过上限"""
|
||||
params = ScoringParams(
|
||||
weight=4.5,
|
||||
bonus_per_unit=2.0,
|
||||
max_bonus_ratio=0.5
|
||||
)
|
||||
result = ScoringService.calculate(
|
||||
ScoringMethod.BONUS,
|
||||
actual_value=10,
|
||||
params=params
|
||||
)
|
||||
|
||||
# 最大加分为权重的50% = 2.25
|
||||
# 总分 = 4.5 + 2.25 = 6.75
|
||||
assert result.score == 6.75
|
||||
assert result.details["status"] == "达到加分上限"
|
||||
|
||||
|
||||
class TestCalculateAssessmentScore:
|
||||
"""考核得分计算测试"""
|
||||
|
||||
def test_calculate_assessment_score(self):
|
||||
"""测试综合考核得分计算"""
|
||||
details = [
|
||||
{
|
||||
"indicator_id": 1,
|
||||
"indicator_name": "业务收支结余率",
|
||||
"bs_dimension": "financial",
|
||||
"weight": 12.6,
|
||||
"actual_value": 18.0,
|
||||
"scoring_method": "target_reference",
|
||||
"scoring_params": {"target_value": 15.0}
|
||||
},
|
||||
{
|
||||
"indicator_id": 2,
|
||||
"indicator_name": "百元收入耗材率",
|
||||
"bs_dimension": "financial",
|
||||
"weight": 12.6,
|
||||
"actual_value": 23.0,
|
||||
"scoring_method": "interval_low",
|
||||
"scoring_params": {"target_value": 25.0}
|
||||
},
|
||||
{
|
||||
"indicator_id": 3,
|
||||
"indicator_name": "病历质量考核",
|
||||
"bs_dimension": "internal_process",
|
||||
"weight": 3.2,
|
||||
"actual_value": 0,
|
||||
"scoring_method": "deduction",
|
||||
"scoring_params": {"deduction_per_unit": 5.0}
|
||||
}
|
||||
]
|
||||
|
||||
result = ScoringService.calculate_assessment_score(details)
|
||||
|
||||
assert "total_score" in result
|
||||
assert "weighted_score" in result
|
||||
assert "dimensions" in result
|
||||
assert result["dimensions"]["financial"]["score"] > 0
|
||||
|
||||
|
||||
class TestInvalidMethod:
|
||||
"""无效方法测试"""
|
||||
|
||||
def test_invalid_method(self):
|
||||
"""测试无效的评分方法"""
|
||||
params = ScoringParams(weight=10.0)
|
||||
|
||||
with pytest.raises(ValueError, match="不支持的评分方法"):
|
||||
ScoringService.calculate(
|
||||
"invalid_method",
|
||||
actual_value=50.0,
|
||||
params=params
|
||||
)
|
||||
279
backend/tests/test_survey_service.py
Normal file
279
backend/tests/test_survey_service.py
Normal file
@@ -0,0 +1,279 @@
|
||||
"""
|
||||
满意度调查服务测试用例
|
||||
|
||||
测试覆盖:
|
||||
1. 问卷管理功能
|
||||
2. 题目管理功能
|
||||
3. 回答提交功能
|
||||
4. 统计分析功能
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from datetime import datetime
|
||||
|
||||
from app.services.survey_service import SurveyService
|
||||
from app.models.models import (
|
||||
Survey, SurveyQuestion, SurveyResponse, SurveyAnswer,
|
||||
SurveyStatus, SurveyType, QuestionType
|
||||
)
|
||||
|
||||
|
||||
class TestSurveyManagement:
|
||||
"""问卷管理测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_survey(self):
|
||||
"""测试创建问卷"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
# 模拟数据库操作
|
||||
survey = MagicMock()
|
||||
survey.id = 1
|
||||
survey.survey_name = "测试问卷"
|
||||
survey.survey_code = "TEST_001"
|
||||
survey.survey_type = SurveyType.INPATIENT
|
||||
survey.status = SurveyStatus.DRAFT
|
||||
|
||||
with patch.object(SurveyService, 'get_survey_by_id', return_value=None):
|
||||
with patch.object(SurveyService, 'get_survey_list', return_value=([], 0)):
|
||||
result = await SurveyService.create_survey(
|
||||
mock_db,
|
||||
survey_name="测试问卷",
|
||||
survey_code="TEST_001",
|
||||
survey_type=SurveyType.INPATIENT
|
||||
)
|
||||
|
||||
mock_db.add.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_publish_survey(self):
|
||||
"""测试发布问卷"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
# 创建模拟问卷
|
||||
survey = MagicMock()
|
||||
survey.id = 1
|
||||
survey.status = SurveyStatus.DRAFT
|
||||
survey.total_questions = 5
|
||||
|
||||
with patch.object(SurveyService, 'get_survey_by_id', return_value=survey):
|
||||
result = await SurveyService.publish_survey(mock_db, 1)
|
||||
|
||||
assert survey.status == SurveyStatus.PUBLISHED
|
||||
mock_db.flush.assert_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_publish_survey_without_questions(self):
|
||||
"""测试发布无题目的问卷(应失败)"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
survey = MagicMock()
|
||||
survey.id = 1
|
||||
survey.status = SurveyStatus.DRAFT
|
||||
survey.total_questions = 0
|
||||
|
||||
with patch.object(SurveyService, 'get_survey_by_id', return_value=survey):
|
||||
with pytest.raises(ValueError, match="问卷没有题目"):
|
||||
await SurveyService.publish_survey(mock_db, 1)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_survey(self):
|
||||
"""测试结束问卷"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
survey = MagicMock()
|
||||
survey.id = 1
|
||||
survey.status = SurveyStatus.PUBLISHED
|
||||
|
||||
with patch.object(SurveyService, 'get_survey_by_id', return_value=survey):
|
||||
result = await SurveyService.close_survey(mock_db, 1)
|
||||
|
||||
assert survey.status == SurveyStatus.CLOSED
|
||||
|
||||
|
||||
class TestQuestionManagement:
|
||||
"""题目管理测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_question(self):
|
||||
"""测试添加题目"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
survey = MagicMock()
|
||||
survey.id = 1
|
||||
survey.status = SurveyStatus.DRAFT
|
||||
survey.total_questions = 0
|
||||
|
||||
with patch.object(SurveyService, 'get_survey_by_id', return_value=survey):
|
||||
# 模拟查询最大排序号
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar.return_value = None
|
||||
|
||||
with patch('app.services.survey_service.select') as mock_select:
|
||||
mock_db.execute.return_value = mock_result
|
||||
|
||||
result = await SurveyService.add_question(
|
||||
mock_db,
|
||||
survey_id=1,
|
||||
question_text="您对服务是否满意?",
|
||||
question_type=QuestionType.SCORE
|
||||
)
|
||||
|
||||
mock_db.add.assert_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_question_to_published_survey(self):
|
||||
"""测试向已发布问卷添加题目(应失败)"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
survey = MagicMock()
|
||||
survey.id = 1
|
||||
survey.status = SurveyStatus.PUBLISHED
|
||||
|
||||
with patch.object(SurveyService, 'get_survey_by_id', return_value=survey):
|
||||
with pytest.raises(ValueError, match="只有草稿状态的问卷可以添加题目"):
|
||||
await SurveyService.add_question(
|
||||
mock_db,
|
||||
survey_id=1,
|
||||
question_text="测试题目",
|
||||
question_type=QuestionType.SCORE
|
||||
)
|
||||
|
||||
|
||||
class TestResponseSubmission:
|
||||
"""回答提交测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_submit_response(self):
|
||||
"""测试提交问卷回答"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
# 创建模拟问卷
|
||||
survey = MagicMock()
|
||||
survey.id = 1
|
||||
survey.status = SurveyStatus.PUBLISHED
|
||||
|
||||
# 创建模拟题目
|
||||
question = MagicMock()
|
||||
question.id = 1
|
||||
question.question_type = QuestionType.SCORE
|
||||
question.score_max = 5
|
||||
question.options = None
|
||||
|
||||
# 设置mock返回值
|
||||
mock_q_result = MagicMock()
|
||||
mock_q_result.scalar_one_or_none.return_value = question
|
||||
|
||||
with patch.object(SurveyService, 'get_survey_by_id', return_value=survey):
|
||||
with patch('app.services.survey_service.select') as mock_select:
|
||||
mock_db.execute.return_value = mock_q_result
|
||||
|
||||
result = await SurveyService.submit_response(
|
||||
mock_db,
|
||||
survey_id=1,
|
||||
department_id=1,
|
||||
answers=[
|
||||
{"question_id": 1, "answer_value": "4"}
|
||||
]
|
||||
)
|
||||
|
||||
mock_db.add.assert_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_submit_response_to_closed_survey(self):
|
||||
"""测试向已结束问卷提交回答(应失败)"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
survey = MagicMock()
|
||||
survey.id = 1
|
||||
survey.status = SurveyStatus.CLOSED
|
||||
|
||||
with patch.object(SurveyService, 'get_survey_by_id', return_value=survey):
|
||||
with pytest.raises(ValueError, match="问卷未发布或已结束"):
|
||||
await SurveyService.submit_response(
|
||||
mock_db,
|
||||
survey_id=1,
|
||||
department_id=1,
|
||||
answers=[]
|
||||
)
|
||||
|
||||
|
||||
class TestSatisfactionStats:
|
||||
"""满意度统计测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_department_satisfaction(self):
|
||||
"""测试获取科室满意度统计"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
# 模拟查询结果
|
||||
mock_result = MagicMock()
|
||||
mock_row = MagicMock()
|
||||
mock_row.department_id = 1
|
||||
mock_row.department_name = "内科"
|
||||
mock_row.response_count = 10
|
||||
mock_row.avg_satisfaction = 85.5
|
||||
mock_row.total_score = 850.0
|
||||
mock_row.max_score = 1000.0
|
||||
mock_result.fetchall.return_value = [mock_row]
|
||||
|
||||
with patch('app.services.survey_service.select') as mock_select:
|
||||
mock_db.execute.return_value = mock_result
|
||||
|
||||
result = await SurveyService.get_department_satisfaction(
|
||||
mock_db,
|
||||
survey_id=1
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["department_name"] == "内科"
|
||||
assert result[0]["avg_satisfaction"] == 85.5
|
||||
|
||||
|
||||
class TestQuestionTypes:
|
||||
"""不同题型测试"""
|
||||
|
||||
def test_score_question_options(self):
|
||||
"""测试评分题选项"""
|
||||
question = MagicMock()
|
||||
question.question_type = QuestionType.SCORE
|
||||
question.score_max = 5
|
||||
question.options = None # 评分题不需要选项
|
||||
|
||||
# 评分题不需要选项
|
||||
assert question.options is None
|
||||
|
||||
def test_single_choice_options(self):
|
||||
"""测试单选题选项"""
|
||||
import json
|
||||
|
||||
options = [
|
||||
{"label": "非常满意", "value": "5", "score": 5},
|
||||
{"label": "满意", "value": "4", "score": 4},
|
||||
{"label": "一般", "value": "3", "score": 3},
|
||||
{"label": "不满意", "value": "2", "score": 2}
|
||||
]
|
||||
|
||||
question = MagicMock()
|
||||
question.question_type = QuestionType.SINGLE_CHOICE
|
||||
question.options = json.dumps(options, ensure_ascii=False)
|
||||
|
||||
parsed_options = json.loads(question.options)
|
||||
assert len(parsed_options) == 4
|
||||
|
||||
def test_multiple_choice_options(self):
|
||||
"""测试多选题选项"""
|
||||
import json
|
||||
|
||||
options = [
|
||||
{"label": "选项A", "value": "a", "score": 1},
|
||||
{"label": "选项B", "value": "b", "score": 1},
|
||||
{"label": "选项C", "value": "c", "score": 1}
|
||||
]
|
||||
|
||||
question = MagicMock()
|
||||
question.question_type = QuestionType.MULTIPLE_CHOICE
|
||||
question.options = json.dumps(options, ensure_ascii=False)
|
||||
|
||||
parsed_options = json.loads(question.options)
|
||||
assert len(parsed_options) == 3
|
||||
Reference in New Issue
Block a user